diff --git a/components/EmailPreview.js b/components/EmailPreview.js index fcb3e7c..68bfbf3 100644 --- a/components/EmailPreview.js +++ b/components/EmailPreview.js @@ -1,8 +1,7 @@ import React, { Component } from 'react' -import { connect } from 'react-redux' +import superConnect from '../utils/superConnect' class EmailPreview extends Component { - render() { console.log("Rendering email preview"); const { email } = this.props; @@ -29,4 +28,8 @@ const mapStateToProps = function(state, existingProps) { } } -export default connect(mapStateToProps)(EmailPreview); +const runSideEffects = function() { + dispatch(emailApp.actions.email.ensureFreshEmails()); +} + +export default superConnect(runSideEffects, mapStateToProps)(EmailPreview); diff --git a/components/Emails.js b/components/Emails.js index e3395af..5e51a16 100644 --- a/components/Emails.js +++ b/components/Emails.js @@ -1,10 +1,9 @@ import React, { Component, PropTypes } from 'react' -import { connect } from 'react-redux' +import superConnect from '../utils/superConnect' import MoveEmail from './MoveEmail' class Emails extends Component { - render() { const { emails, fetchedAt } = this.props; return ( @@ -27,9 +26,9 @@ class Emails extends Component { {email.subject} {email.sender} - {email.folderName} + {email.folder ? email.folder.name : 'Missing...'} - + ) }) @@ -42,11 +41,17 @@ class Emails extends Component { } const mapStateToProps = function(state) { + const emailState = state.emailApp.emails; + let folders = state.emailApp.folders.folders; + let emails = emailState.emails; + if(emails && folders) { + emails = emails.map((email) => { + return Object.assign({}, email, { folder: folders.find((folder) => folder.id === email.folderId) }); + }); + } return { - emails: state.emailApp.emails.emails.map((email) => { - return Object.assign({}, email, { folderName: state.emailApp.folders.folders.find((folder) => folder.id === email.folderId).name }); - }), - fetchedAt: state.emailApp.emails.fetchedAt + emails: emails, + fetchedAt: emailState.fetchedAt } } @@ -57,4 +62,9 @@ const mapDispatchToProps = function(dispatch) { } } -export default connect(mapStateToProps, mapDispatchToProps)(Emails); +const runSideEffects = function(state, dispatch) { + dispatch(emailApp.actions.folder.ensureFreshFolders()); + dispatch(emailApp.actions.email.ensureFreshEmails()); +} + +export default superConnect(runSideEffects, mapStateToProps, mapDispatchToProps)(Emails); diff --git a/components/Folder.js b/components/Folder.js index d644fa6..88461c2 100644 --- a/components/Folder.js +++ b/components/Folder.js @@ -1,55 +1,59 @@ import React, { Component, PropTypes } from 'react' -import { connect } from 'react-redux' +import superConnect from '../utils/superConnect' import { Link } from 'react-router' import emailApp from '../emailApp' import * as actions from '../actions' import MoveEmail from './MoveEmail' - class Folder extends Component { - render() { const { emails, folder, fetchedAt } = this.props; return ( -
-

{folder.name} (fetched at {fetchedAt}

- - - - - - - - - - - - { - emails.map((email) => { - return ( - - - - - - - - ) - }) - } - -
SubjectSenderDeleteMoveOpen
{email.subject}{email.sender}
- {this.props.children} -
+ folder !== undefined && emails !== undefined ? ( +
+

{folder.name} (fetched at {fetchedAt})

+ + + + + + + + + + + + { + emails.map((email) => { + return ( + + + + + + + + ) + }) + } + +
SubjectSenderDeleteMoveOpen
{email.subject}{email.sender}
+ {this.props.children} +
+ ) :

Loading...

) } } const mapStateToProps = function(state, existingProps) { + const foldersState = state.emailApp.folders; + const emailsState = state.emailApp.emails; + const folder = foldersState.folders ? foldersState.folders.find((folder) => folder.id === existingProps.params.folderId) : undefined; + const emails = emailsState.emails ? emailsState.emails.filter((email) => email.folderId === existingProps.params.folderId) : undefined; return { - folder: state.emailApp.folders.folders.find((folder) => folder.id === existingProps.params.folderId), - emails: state.emailApp.emails.emails.filter((email) => email.folderId === existingProps.params.folderId), - fetchedAt: state.emailApp.emails.fetchedAt + folder: folder, + emails: emails, + fetchedAt: emailsState.fetchedAt } } @@ -61,4 +65,9 @@ const mapDispatchToProps = function(dispatch) { } } -export default connect(mapStateToProps, mapDispatchToProps)(Folder); +const runSideEffects = function(state, dispatch) { + dispatch(emailApp.actions.folder.ensureFreshFolders()); + dispatch(emailApp.actions.email.ensureFreshEmails()); +} + +export default superConnect(runSideEffects, mapStateToProps, mapDispatchToProps)(Folder); diff --git a/components/Folders.js b/components/Folders.js index a03d1ae..b6d8b82 100644 --- a/components/Folders.js +++ b/components/Folders.js @@ -1,29 +1,31 @@ import React, { Component, PropTypes } from 'react' -import { connect } from 'react-redux' +import superConnect from '../utils/superConnect' import AddFolder from './AddFolder' import emailApp from '../emailApp' class Folders extends Component { - render() { const { folders } = this.props; return ( -
-

Folders

-
    - { folders.map((folder) =>
  • {folder.name} -
  • ) } -
- -
+ folders ? ( +
+

Folders

+
    + { folders.map((folder) =>
  • {folder.name} -
  • ) } +
+ +
+ ) :

Loading...

) } } const mapStateToProps = function(state) { + const foldersState = state.emailApp.folders; return { - folders: state.emailApp.folders.folders, - fetchedAt: state.emailApp.folders.fetchedAt + folders: foldersState.folders, + fetchedAt: foldersState.fetchedAt } } @@ -33,4 +35,8 @@ const mapDispatchToProps = function(dispatch) { } } -export default connect(mapStateToProps, mapDispatchToProps)(Folders); +const runSideEffects = function(state, dispatch) { + dispatch(emailApp.actions.folder.ensureFreshFolders()); +} + +export default superConnect(runSideEffects, mapStateToProps, mapDispatchToProps)(Folders); diff --git a/components/MoveEmail.js b/components/MoveEmail.js index 93e7c96..63694b2 100644 --- a/components/MoveEmail.js +++ b/components/MoveEmail.js @@ -1,5 +1,5 @@ import React, { Component, PropTypes } from 'react' -import { connect } from 'react-redux' +import superConnect from '../utils/superConnect' import emailApp from '../emailApp' @@ -25,7 +25,11 @@ class MoveEmail extends Component { return (
this.submit(e)}>
@@ -34,17 +38,20 @@ class MoveEmail extends Component { } const mapStateToProps = function(state, existingProps) { - const email = state.emailApp.emails.emails.find((email) => email.id === existingProps.emailId); + const foldersState = state.emailApp.folders; return { - email: email, - folders: state.emailApp.folders.folders + folders: foldersState.folders } } const mapDispatchToProps = function(dispatch, existingProps) { return { - moveToFolder: (folderId) => dispatch(emailApp.actions.email.moveEmailToFolder(existingProps.emailId, folderId)) + moveToFolder: (folderId) => dispatch(emailApp.actions.email.moveEmailToFolder(existingProps.email.id, folderId)) } } -export default connect(mapStateToProps, mapDispatchToProps)(MoveEmail); +const runSideEffects = function(state, dispatch) { + dispatch(emailApp.actions.folder.ensureFreshFolders()); +} + +export default superConnect(runSideEffects, mapStateToProps, mapDispatchToProps)(MoveEmail); diff --git a/components/OpenEmail.js b/components/OpenEmail.js index f542291..cdd39e2 100644 --- a/components/OpenEmail.js +++ b/components/OpenEmail.js @@ -1,5 +1,7 @@ import React, { Component } from 'react' -import { connect } from 'react-redux' +import superConnect from '../utils/superConnect' + +import emailApp from '../emailApp' class OpenEmail extends Component { @@ -20,4 +22,8 @@ const mapStateToProps = function(state, existingProps) { } } -export default connect(mapStateToProps)(OpenEmail); +const runSideEffects = function(state, dispatch) { + dispatch(emailApp.actions.email.ensureFreshEmails()); +} + +export default superConnect(runSideEffects, mapStateToProps)(OpenEmail); diff --git a/containers/App.js b/containers/App.js index 5f31e0e..e91282e 100644 --- a/containers/App.js +++ b/containers/App.js @@ -1,9 +1,11 @@ import React, { Component, PropTypes } from 'react' -import { connect } from 'react-redux' +import superConnect from '../utils/superConnect' import Folders from '../components/Folders' import OpenEmails from '../components/OpenEmails.js'; import { Link } from 'react-router' +import emailApp from '../emailApp'; + class App extends Component { render() { @@ -17,11 +19,13 @@ class App extends Component { Folders @@ -37,10 +41,15 @@ class App extends Component { {this.props.children} + ) } } +const runSideEffects = function(state, dispatch) { + dispatch(emailApp.actions.folder.ensureFreshFolders()); +} + // Wrap the component to inject dispatch and state into it -export default connect((state) => { return { folders: state.emailApp.folders.folders } })(App) +export default superConnect(runSideEffects, (state) => { return { folders: state.emailApp.folders.folders } })(App) diff --git a/emailApp/actions/emailActions.js b/emailApp/actions/emailActions.js index e7c7bf8..c24ad36 100644 --- a/emailApp/actions/emailActions.js +++ b/emailApp/actions/emailActions.js @@ -3,8 +3,18 @@ import { api } from '../api' window.api = api; export const FETCHED_EMAILS = 'FETCHED_EMAILS' +export const REMOVED_EMAIL = 'REMOVED_EMAIL' +export const MOVED_EMAIL_TO_FOLDER = 'MOVED_EMAIL_TO_FOLDER' + +// State actions +const removedEmail = function(emailId) { + return { type: REMOVED_EMAIL, emailId: emailId }; +} + +const movedEmailToFolder = function(emailId, folderId) { + return { type: MOVED_EMAIL_TO_FOLDER, emailId: emailId, folderId: folderId }; +} -// State action const fetchedEmails = function(emails, fetchedAt) { return { type: FETCHED_EMAILS, emails: emails, fetchedAt: fetchedAt }; } @@ -17,16 +27,24 @@ export function fetchEmails() { } } +export function ensureFreshEmails() { + return (dispatch, getState) => { + if(getState().emailApp.emails.dirty) { + dispatch(fetchEmails()); + } + } +} + export function removeEmail(emailId) { return (dispatch) => { api.removeEmail(emailId); - dispatch(fetchEmails()); + dispatch(removedEmail(emailId)); } } export function moveEmailToFolder(emailId, folderId) { return (dispatch) => { api.moveEmailToFolder(emailId, folderId); - dispatch(fetchEmails()); + dispatch(movedEmailToFolder(emailId, folderId)); } } diff --git a/emailApp/actions/folderActions.js b/emailApp/actions/folderActions.js index a85fc51..39e2014 100644 --- a/emailApp/actions/folderActions.js +++ b/emailApp/actions/folderActions.js @@ -3,11 +3,21 @@ import { api } from '../api' import * as emailActions from './emailActions' export const FETCHED_FOLDERS = 'FETCHED_FOLDERS' +export const ADDED_FOLDER = 'ADDED_FOLDER' +export const REMOVED_FOLDER = 'REMOVED_FOLDER' const fetchedFolders = function(folders, fetchedAt) { return { type: FETCHED_FOLDERS, folders: folders, fetchedAt: fetchedAt }; } +const removedFolder = function(folderId) { + return { type: REMOVED_FOLDER, folderId: folderId }; +} + +const addedFolder = function() { + return { type: ADDED_FOLDER }; +} + // Public application API for consumption by GUI export const fetchFolders = function() { @@ -17,17 +27,24 @@ export const fetchFolders = function() { } } +export const ensureFreshFolders = function() { + return (dispatch, getState) => { + if(getState().emailApp.folders.dirty) { + dispatch(fetchFolders()); + } + } +} + export const addFolder = function(name) { return (dispatch) => { - api.addFolder(name) - dispatch(fetchFolders()); + api.addFolder(name); + dispatch(addedFolder()); } } export const removeFolder = function(folderId) { return (dispatch) => { api.removeFolder(folderId); - dispatch(emailActions.fetchEmails()); - dispatch(fetchFolders()); + dispatch(removedFolder(folderId)); } } diff --git a/emailApp/reducers/emailsReducer.js b/emailApp/reducers/emailsReducer.js index cbe5a77..5a2b698 100644 --- a/emailApp/reducers/emailsReducer.js +++ b/emailApp/reducers/emailsReducer.js @@ -1,9 +1,21 @@ -import { FETCHED_EMAILS } from '../actions/emailActions.js'; +import { REMOVED_EMAIL, MOVED_EMAIL_TO_FOLDER, FETCHED_EMAILS, fetchEmails } from '../actions/emailActions.js' +import { REMOVED_FOLDER } from '../actions/folderActions.js'; -const emailsReducer = function(state = [], action) { +const initialState = { + emails: [], + dirty: true +} + +const emailsReducer = function(state = initialState, action) { switch(action.type) { + case REMOVED_FOLDER: + case REMOVED_EMAIL: + case MOVED_EMAIL_TO_FOLDER: + console.log("Marking emails as dirty"); + return Object.assign({}, state, { dirty: true }); case FETCHED_EMAILS: - return { emails: action.emails, fetchedAt: action.fetchedAt } + console.log("Fetched emails in emails reducer"); + return { emails: action.emails, dirty: false, fetchedAt: action.fetchedAt }; default: return state; } diff --git a/emailApp/reducers/foldersReducer.js b/emailApp/reducers/foldersReducer.js index dc00463..365add2 100644 --- a/emailApp/reducers/foldersReducer.js +++ b/emailApp/reducers/foldersReducer.js @@ -1,9 +1,19 @@ -import { FETCHED_FOLDERS } from '../actions/folderActions.js'; +import { ADDED_FOLDER, REMOVED_FOLDER, FETCHED_FOLDERS, fetchFolders } from '../actions/folderActions.js'; -const foldersReducer = function(state = [], action) { +const initialState = { + folders: null, + dirty: true +} + +const foldersReducer = function(state = initialState, action) { switch(action.type) { + case ADDED_FOLDER: + case REMOVED_FOLDER: + console.log("Added or Removed folder in folders reducer"); + return Object.assign({}, state, { dirty: true }); case FETCHED_FOLDERS: - return { folders: action.folders, fetchedAt: action.fetchedAt }; + console.log("Fetched folders in folders reducer"); + return { folders: action.folders, dirty: false }; default: return state; } diff --git a/emailApp/reducers/index.js b/emailApp/reducers/index.js index ecaf46d..cda92d0 100644 --- a/emailApp/reducers/index.js +++ b/emailApp/reducers/index.js @@ -5,7 +5,7 @@ import foldersReducer from '../reducers/foldersReducer.js'; const rootReducer = combineReducers({ emails: emailsReducer, - folders: foldersReducer + folders: foldersReducer, }); export default rootReducer; diff --git a/index.js b/index.js index 42af046..a63cc42 100644 --- a/index.js +++ b/index.js @@ -5,14 +5,17 @@ import { DevTools, LogMonitor, DebugPanel } from 'redux-devtools/lib/react'; import App from './containers/App' import configureStore, { USE_DEV_TOOLS } from './store/configureStore' import { Route, Router as RealRouter } from 'react-router' + import * as actions from './actions/'; import emailApp from './emailApp' + import Folders from './components/Folders' import Folder from './components/Folder' import Emails from './components/Emails' import EmailPreview from './components/EmailPreview' import Counter from './components/Counter' -import generatePageLoaders from './pageLoaders' + +import createBrowserHistory from 'history/lib/createBrowserHistory' class Router extends RealRouter { render() { @@ -28,11 +31,11 @@ class Router extends RealRouter { } } + + window.emailApp = emailApp; const store = configureStore(); -const pageLoaders = generatePageLoaders(store.dispatch); - const debugPanel = USE_DEV_TOOLS ? ( @@ -43,12 +46,12 @@ let rootElement = document.getElementById('root') render(
- - - - - - + + console.log("onEnter for App")}> + console.log("onEnter for Folders")}/> + console.log("onEnter for emails")}/> + console.log("onEnter for folder")}> + console.log("onEnter for EmailPreview")}/> store.dispatch(actions.initializeCounter())}/> diff --git a/pageLoaders/index.js b/pageLoaders/index.js index 8f7631d..e69de29 100644 --- a/pageLoaders/index.js +++ b/pageLoaders/index.js @@ -1,23 +0,0 @@ -import emailApp from '../emailApp' - -export default function generatePageLoaders(dispatch) { - - return { - - appShow: function() { - dispatch(emailApp.actions.folder.fetchFolders()); - }, - - emailsIndex: function() { - dispatch(emailApp.actions.email.fetchEmails()); - }, - - foldersIndex: function() { - dispatch(emailApp.actions.folder.fetchFolders()); - }, - - folderShow: function() { - dispatch(emailApp.actions.email.fetchEmails()); - } - } -} diff --git a/store/configureStore.js b/store/configureStore.js index 420b5aa..0b3987e 100644 --- a/store/configureStore.js +++ b/store/configureStore.js @@ -3,7 +3,7 @@ import rootReducer from '../reducers' import { devTools } from 'redux-devtools'; import thunk from 'redux-thunk'; -export const USE_DEV_TOOLS = true; +export const USE_DEV_TOOLS = false; export default function configureStore(initialState) { let composed = compose(applyMiddleware(thunk)); diff --git a/utils/superConnect.js b/utils/superConnect.js new file mode 100644 index 0000000..c6d1476 --- /dev/null +++ b/utils/superConnect.js @@ -0,0 +1,32 @@ +import { connect } from 'react-redux'; + +// All of this is just monkey patching handleChange() in the connected +// component so that it calls the `runSideEffects` function each time +// the store notifies the component that the store state has changed. + +const superConnect = function(runSideEffects, mapStateToProps, mapDispatchToProps, mergeProps, options) { + const wrapWithConnect = connect(mapStateToProps, mapDispatchToProps, mergeProps, options) + return function(WrappedComponent) { + const ConnectedComponent = wrapWithConnect(WrappedComponent); + class SuperConnectedComponent extends ConnectedComponent { + + handleChange() { + // Original behavior: + if (!this.unsubscribe) { + return + } + + this.setState({ + storeState: this.store.getState() + }) + + // Additional behavior: + runSideEffects(this.store.getState(), this.store.dispatch); + } + } + + return SuperConnectedComponent; + } +} + +export default superConnect