redux-async-props

0.2.1 • Public • Published

redux-async-props

A simple way to load props and state asynchonously with react-router and redux.

Why?

Often pages will have a bunch of resources they need to fetch before rendering properly. The easy way to do this is add all your fetching logic to the componentDidMount method. However this offers a poor user experience as the user has to wait for the page to load and then wait again for the data to be fetched. The best way is to have everything that's needed already loaded in the initial redux state and passed in as props on the first render.

How to use

Define what each component needs with a needs property. This should be a function that accepts the router props and then the redux store object. e.g.

MyComponent.needs = (props, store) => 
  store.dispatch(doMyAsyncAction())
  .then(() => {
    var state = store.getState()
    return {
      myData: state.myAsyncData,
    }
})

Why do we pass the store in and not the state? This allows more freedom, you can construct the promise in any way shape or form that makes sense for optimal data loading. If you want to return new props to the component just be sure to return an object at the end of the promise chain.

Any redux actions you dispatch here will also populate your redux store, which means you could use the react-redux connect decorator to map your redux state to props on the first render if that's your preferred workflow.

You'll need to configure your server and client. See the next steps below, the steps assume you're familiar with how to setup redux and react-router, if you're new to these two packages it might be best to start with their tutorials first.

Configuring the server

import { fetchNeeds, AsyncRouterContext } from 'redux-async-props'
// ...
app.get('*', function (req, res, next) {
  match({ routes: routes, location: req.url }, (err, redirect, props) => {
  const store = createStore(reducer, applyMiddleware(callAPIMiddleware))
    fetchNeeds(props, store)
    // This param contains the fetched async props for each route
    .then((asyncProps) => {
      const appHtml = renderToString(
          // Ensure you wrap inside a redux Provider as the AsyncRouterContext 
          // depends on having the redux store in the context.
        <Provider store={store}>
            {/* We need to use the AsyncRouterContext so it can handle using
              * the async props on the initial render. */}
          <AsyncRouterContext {...props} asyncProps={asyncProps} />
      </Provider>
      )
      // dump the HTML into a template, lots of ways to do this.
      var html = require("raw!./public/index.html")
      html = html.replace('<!--__APP_HTML__-->', appHtml)
      // Our initialState is slightly different to the norm, instead of being 
      // made up of only the redux store, it now also contains the async props 
      // for the initial render (we'll use these on the client to avoid 
      // rendering twice).
      const initialState = {asyncProps, store: store.getState()}
      html = html.replace('{/*__INITIAL_STATE__*/}', JSON.stringify(initialState))
      res.send(html)
    })
  })
})

Configuring the client

import { AsyncRouterContext } from 'redux-async-props'
//...
const initialState = window.__INITIAL_STATE__
const store = createStore(reducer, initialState.store)
render((
  <Provider store={store}>
    <Router 
      routes={routes} 
      history={browserHistory}
      render={(props) => <AsyncRouterContext 
        {...props} 
        // Pass in the async props that we're hydrating from
        // the server, these are needed so that the initial render
        // only needs to be done once.
        asyncProps={initialState.asyncProps}
      />}
    />
  </Provider>
), document.getElementById('app'))

The client is rendered synchonously so we can't wait for any data to be fetched. This is why we store the initial async props that are generated on the server and then hydrate them for the first render.

Navigating to new pages will execute each new component's needs. You can minimise the number of requests sent by checking if your redux state already contains the data needed. If so don't go and fetch it from the server again.

Things that have to work

Your asynchronous actions have to run on both the client and server. If your using fetch then the isomorphic-fetch is a pretty good package for ensuring it works on the server too.

Usage with react-redux

It's safe to use with the connect decorator as this can just be used as a way to pre-populate the redux store. Then you can use connect to map the store's state to props if you so wish.

Examples

See the example directory for a full blown example of all the above put into action.

Package Sidebar

Install

npm i redux-async-props

Weekly Downloads

16

Version

0.2.1

License

ISC

Last publish

Collaborators

  • alastair