-
-
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