-
-
Save andrewmclagan/c4e84b0dd76e721cf75db1c06439a19b to your computer and use it in GitHub Desktop.
export function login(loginHandle, password) { | |
return { | |
types: [LOGIN_REQUEST, LOGIN_SUCCESS, LOGIN_FAILURE], | |
promise: (api) => api.post('/auth/login', { login: loginHandle, password }).then(response => { | |
setAuthCookie(response.token); // side effect pre success dispatch | |
return response; | |
}), | |
then: (response) => { | |
postLoginRedirect(browserHistory.push, response.user, response.organisation); // side effect post success dispatch | |
}, | |
}; | |
} |
export default function asyncThunkMiddleware(api) { | |
return ({ dispatch, getState }) => { | |
return next => action => { | |
// #1 Enable traditional redux-thunks | |
if (typeof action === 'function') { | |
return action(dispatch, getState); | |
} | |
const { promise, then, types, ...rest } = action; // eslint-disable-line no-redeclare | |
// #2 Dispatch normal actions and skip this middleware | |
if (!promise) { | |
return next(action); | |
} | |
// #3 Create mock after function | |
if (!then) { | |
let then = () => {}; // eslint-disable-line | |
} | |
const [REQUEST, SUCCESS, FAILURE] = types; | |
// #4 Dispatch the request action | |
next({ ...rest, type: REQUEST }); | |
// #5 Execute the async api call and get returned promise | |
const actionPromise = promise(api(dispatch, getState), dispatch, getState); | |
actionPromise | |
.then((response) => { | |
// #6 Dispatch the success action with response | |
next({ ...rest, ...response, type: SUCCESS }); | |
// #7 Call after and pass along promise, allowing the thunk to execute "after" side effects | |
then(response, dispatch, getState); | |
}) | |
.catch((error) => { | |
// #8 Dispatch the error action with response | |
next({ ...rest, error, type: FAILURE }); | |
// #9 Call after and pass along promise, allowing the thunk to execute "after" side effects | |
then(error, dispatch, getState); | |
}); | |
return actionPromise; | |
}; | |
}; | |
} |
import 'babel-polyfill'; | |
import Express from 'express'; | |
import React from 'react'; | |
import { renderToString } from 'react-dom/server'; | |
import config from './config'; | |
import favicon from 'serve-favicon'; | |
import compression from 'compression'; | |
import httpProxy from 'http-proxy'; | |
import path from 'path'; | |
import createStore from './redux/create'; | |
import { api } from 'utilities/api'; | |
import { Html } from 'containers'; | |
import http from 'http'; | |
import cookieParser from 'cookie-parser'; | |
import { match, createMemoryHistory, RouterContext } from 'react-router'; | |
import { syncHistoryWithStore } from 'react-router-redux'; | |
import { Provider } from 'react-redux'; | |
import getRoutes from './routes'; | |
import { LOAD_RECIEVE } from 'redux/modules/auth/auth'; | |
import { ENUMS_RECIEVE } from 'redux/modules/app'; | |
/* | |
|-------------------------------------------------------------------------- | |
| Server configuration / setup | |
|-------------------------------------------------------------------------- | |
*/ | |
const app = new Express(); | |
const server = new http.Server(app); | |
const proxy = httpProxy.createProxyServer({ | |
target: `http://${config.apiHost}:${config.apiPort}`, | |
changeOrigin: true, | |
}); | |
app.use(compression()); | |
app.use(favicon(path.join(__dirname, '..', 'static', 'favicon.ico'))); | |
app.use(Express.static(path.join(__dirname, '..', 'static'))); | |
app.use(cookieParser()); | |
/* | |
|-------------------------------------------------------------------------- | |
| Utility functions | |
|-------------------------------------------------------------------------- | |
*/ | |
/** | |
* Returns token from request cookie if present | |
* | |
* @return Object | |
*/ | |
function authTokenFromRequest(request) { | |
return request.cookies._token ? request.cookies._token : ''; | |
} | |
/** | |
* Returns initial state from the API server | |
* | |
* @return Object | |
*/ | |
function fetchInitialState(token) { | |
const getState = () => { return { auth: { token } }; }; | |
const mockDispatch = () => {}; | |
return api(mockDispatch, getState).get('/initialize'); | |
} | |
/** | |
* Dispatches initial state to the store | |
* | |
* @return Object | |
*/ | |
function dispatchInitialState(dispatch, fetchedState) { | |
if (fetchedState.auth) { | |
dispatch({ type: LOAD_RECIEVE, response: fetchedState }); | |
} | |
if (fetchedState.enumerables) { | |
dispatch({ type: ENUMS_RECIEVE, response: fetchedState }); | |
} | |
} | |
/** | |
* Renders HTML to string | |
* | |
* @return String | |
*/ | |
function renderHtml(renderer, store, component) { | |
const html = renderer(<Html assets={webpackIsomorphicTools.assets()} component={component} store={store} />); | |
return `<!doctype html> \n ${html}`; | |
} | |
/** | |
* Initializes the application | |
* | |
* @return String | |
*/ | |
function initializeApp(request, response, api) { | |
const memoryHistory = createMemoryHistory(request.url); | |
const store = createStore(memoryHistory, api); | |
const history = syncHistoryWithStore(memoryHistory, store); | |
const token = authTokenFromRequest(request); | |
fetchInitialState(token) | |
.catch(error => console.log('>> User has no auth: ', error)) | |
.then(fetchedState => { | |
dispatchInitialState(store.dispatch, fetchedState); | |
match({ routes: getRoutes(store), location: request.url, history }, (error, redirect, renderProps) => { | |
const component = ( | |
<Provider store={store} key="provider"> | |
<RouterContext {...renderProps} /> | |
</Provider> | |
); | |
global.navigator = { userAgent: request.headers['user-agent'] }; | |
if (error) { | |
response.status(500).send(error.message); | |
} else if (redirect) { | |
response.redirect(302, `${redirect.pathname} ${redirect.search}`); | |
} else if (renderProps) { | |
response.status(200).send(renderHtml(renderToString, store, component)); | |
} else { | |
response.status(404).send('Page not found.'); | |
} | |
}); | |
}); | |
} | |
/* | |
|-------------------------------------------------------------------------- | |
| Server routes | |
|-------------------------------------------------------------------------- | |
*/ | |
/** | |
* API proxy route | |
* | |
* @return Void | |
*/ | |
app.use('/api', (request, response) => { | |
proxy.web(request, response); | |
}); | |
/** | |
* Proxy error callback route | |
* | |
* @return Void | |
*/ | |
proxy.on('error', (error, request, response) => { | |
if (error.code !== 'ECONNRESET') { | |
console.error('proxy error', error); | |
} | |
if (! response.headersSent) { | |
response.writeHead(500, { 'content-type': 'application/json' }); | |
} | |
response.end(JSON.stringify({ error: 'proxy_error', reason: error.message })); | |
}); | |
/** | |
* Healthcheck route | |
* | |
* @return Void | |
*/ | |
app.get('/health-check', (request, response) => { | |
response.status(200).send('Everything is just fine...'); | |
}); | |
/** | |
* React application render route | |
* | |
* @return Void | |
*/ | |
app.use((request, response) => { | |
if (__DEVELOPMENT__) { | |
// Do not cache webpack stats: the script file would change since, hot module replacement is enabled in the development env | |
webpackIsomorphicTools.refresh(); | |
} | |
initializeApp(request, response, api); | |
}); | |
/* | |
|-------------------------------------------------------------------------- | |
| Init server | |
|-------------------------------------------------------------------------- | |
*/ | |
server.listen(config.port, (error) => { | |
if (error) { | |
console.error(error); | |
} | |
console.info('>> Server running at http://%s:%s', config.host, config.port); | |
}); |
Hi Andrew, very nicely done. Looks to be much more testable as you mention. Any chance of adding a splattering of your utilities/api to see your approach there? I watched some of Dan's latest on idiomatic redux and see where the approach has come from. I really like this updated way of using redux-thunk. Well done and thanks for sharing.
👍
Thanks so much for sharing this!
@andrewmclagan, +1 to @tzarger's request above. I'm also curious to see utilities/api and how you changed your client.js that deals with the hydration and creating of the store.
this is a gem! I wish I had seen this -2 days ago 💯 Also it'd be good to see the API
I've modified this to have dispatch consistently return Promises & changed from using then for both success and failure to be separate functions. https://github.com/davidfurlong/redux-triple-barreled-actions
A functional approach.
We have test coverage for all the above functions.
Far far easier to read IMO then the spaghetti that was previously there. No offence intended