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}
-
-
-
- Subject |
- Sender |
- Delete |
- Move |
- Open |
-
-
-
- {
- emails.map((email) => {
- return (
-
- {email.subject} |
- {email.sender} |
- |
- |
- |
-
- )
- })
- }
-
-
- {this.props.children}
-
+ folder !== undefined && emails !== undefined ? (
+
+
{folder.name} (fetched at {fetchedAt})
+
+
+
+ Subject |
+ Sender |
+ Delete |
+ Move |
+ Open |
+
+
+
+ {
+ emails.map((email) => {
+ return (
+
+ {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 (
@@ -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
{
- folders.map((folder) => {
- return (
- - {folder.name}
- )
- })
+ folders ? (
+ folders.map((folder) => {
+ return (
+ - {folder.name}
+ )
+ })
+ ) : - Loading 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