Last active
March 12, 2023 12:23
-
-
Save andrewmclagan/c4e84b0dd76e721cf75db1c06439a19b to your computer and use it in GitHub Desktop.
react-redux-universal-hot-example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
}, | |
}; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
}; | |
}; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
}); |
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
this is a gem! I wish I had seen this -2 days ago 💯 Also it'd be good to see the API