Skip to content

Instantly share code, notes, and snippets.

@simenbrekken
Last active April 10, 2017 11:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save simenbrekken/ff49428c2ce7676c032318cf16e7a8b4 to your computer and use it in GitHub Desktop.
Save simenbrekken/ff49428c2ce7676c032318cf16e7a8b4 to your computer and use it in GitHub Desktop.
Universal Rendering with Redux Query and React Router v4
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
}
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)
}
})
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