Last active
April 10, 2017 11:02
-
-
Save simenbrekken/ff49428c2ce7676c032318cf16e7a8b4 to your computer and use it in GitHub Desktop.
Universal Rendering with Redux Query and React Router v4
This file contains 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
let pendingPromises | |
let resolving = false | |
export const registerRequestPromises = createRequestPromises => { | |
if (resolving) { | |
const promises = createRequestPromises() | |
if (promises) { | |
pendingPromises.push(...promises) | |
} | |
} | |
} | |
const performIteration = async (resolver, iterations, maxIterations) => { | |
pendingPromises = [] | |
let result = resolver() | |
if (iterations < maxIterations && pendingPromises.length > 0) { | |
await Promise.all(pendingPromises) | |
result = performIteration(resolver, iterations + 1, maxIterations) | |
} | |
return result | |
} | |
export const resolveRequestPromises = async (resolver, { maxIterations = 1 } = {}) => { | |
resolving = true | |
const result = await performIteration(resolver, 0, maxIterations) | |
resolving = false | |
return result | |
} |
This file contains 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
app.use(async (req, res, next) => { | |
const store = createStore() | |
const context = {} | |
const application = ( | |
<Provider store={store}> | |
<StaticRouter location={req.url} context={context}> | |
<Application /> | |
</StaticRouter> | |
</Provider> | |
) | |
try { | |
const markup = await resolveRequestPromises(() => renderToString(application), { maxIterations: 3 }) | |
const { url, status } = context | |
if (url) { | |
res.redirect(302, url) | |
return | |
} | |
if (status) { | |
res.status(status) | |
} | |
res.send(renderToStaticMarkup( | |
<Document initialState={store.getState()}>{markup}</Document> | |
)) | |
} catch (error) { | |
next(error) | |
} | |
}) |
This file contains 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 React, { PureComponent } from 'react' | |
import { func, shape } from 'prop-types' | |
import { cancelQuery, requestAsync } from 'redux-query' | |
import { difference, intersection, keys, pick, reduce } from 'lodash' | |
import { wrapDisplayName } from 'recompose' | |
import { registerRequestPromises } from '../utils/request-resolver' | |
const createQueries = (mapPropsToQueries, props) => { | |
const queries = mapPropsToQueries(props) | |
// Support both a single flat query and multiple named queries | |
return queries && queries.url && queries.update ? { queries } : queries | |
} | |
const withReduxQuery = mapPropsToQueries => WrappedComponent => { | |
class WithReduxQuery extends PureComponent { | |
static displayName = wrapDisplayName(WrappedComponent, 'withReduxQuery') | |
static contextTypes = { | |
store: shape({ | |
dispatch: func.isRequired, | |
}), | |
} | |
constructor() { | |
super() | |
this.pendingRequests = {} | |
} | |
componentWillMount() { | |
registerRequestPromises(() => { | |
const queries = createQueries(mapPropsToQueries, this.props) | |
return this.dispatchQueries(queries) | |
}) | |
} | |
componentDidMount() { | |
const queries = createQueries(mapPropsToQueries, this.props) | |
this.dispatchQueries(queries) | |
} | |
componentWillReceiveProps(nextProps) { | |
const previousQueries = createQueries(mapPropsToQueries, this.props) | |
const nextQueries = createQueries(mapPropsToQueries, nextProps) | |
const previousKeys = keys(previousQueries) | |
const nextKeys = keys(nextQueries) | |
const intersectingKeys = intersection(previousKeys, nextKeys) | |
const removedKeys = difference(previousKeys, intersectingKeys) | |
const addedKeys = difference(nextKeys, intersectingKeys) | |
const removedQueries = pick(previousQueries, removedKeys) | |
const addedQueries = pick(nextQueries, addedKeys) | |
this.cancelQueries(removedQueries) | |
this.dispatchQueries(addedQueries) | |
} | |
componentWillUnmount() { | |
const queries = createQueries(mapPropsToQueries, this.props) | |
this.cancelQueries(queries) | |
} | |
dispatchQueries(queries, force = false, retry = true) { | |
const { dispatch } = this.context.store | |
return reduce(queries, (promises, query) => { | |
const key = query.queryKey || query.url | |
const promise = dispatch(requestAsync({ | |
force, | |
retry, | |
...query, | |
})) | |
if (promise) { | |
console.log('Dispatched query:', key, 'force:', force, 'retry:', retry) | |
this.pendingRequests[key] = query | |
return promises.concat(promise.then(result => { | |
delete this.pendingRequests[key] | |
return result | |
})) | |
} | |
return promises | |
}, []) | |
} | |
cancelQueries(queries) { | |
const { dispatch } = this.context.store | |
return reduce(queries, (results, query) => { | |
const key = query.queryKey || query.url | |
const request = this.pendingRequests[key] | |
if (request) { | |
return results.concat(dispatch(cancelQuery(key))) | |
} | |
return results | |
}, []) | |
} | |
refetch(keys) { | |
const queries = createQueries(mapPropsToQueries, this.props) | |
const refetchQueries = keys ? pick(queries, keys) : queries | |
return this.dispatchQueries(refetchQueries, true, false) | |
} | |
render() { | |
return ( | |
<WrappedComponent {...this.props} refetch={this.refetch} /> | |
) | |
} | |
} | |
return WithReduxQuery | |
} | |
export default withReduxQuery |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment