Reactjs – Async data flow in React App with Redux + ReactRouter

react-routerreactjsredux

I'm using Redux and React-router to make a simple Mail app. Since I'm rather new to Redux, I'm not quite understand actual data flow in Redux + Router.

What I'm trying to get

  1. After page starts (/), MailListComponent fetches array of messages from server. At this time MessageComponent is not displayed, as it has no single message to fetch data for it.
  2. After state.messages:[] is fetched, app is navigated to the first message of state.messages:[] (/messages/1)`.
  3. After transition is finished, MessageComponent is shown and fetches message with id=1 info and it's in a separate request it's attachments.

Here's the component model:

Component model

What I'm doing

// MailListActions.js
export function loadMessages() {
  return {
    type:    'LOAD_MESSAGES',
    promise: client => client.get('/messages')
  };
}

// MailListReducer.js
import Immutable from 'immutable';

const defaultState = { messages: [], fetchingMessages: false };

export default function mailListReducer(state = defaultState, action = {}) {
  switch (action.type) {
    case 'LOAD_MESSAGES_REQUEST':
      return state.merge({fetchingMessages: true});

    case 'LOAD_MESSAGES':
        return state.merge({fetchingMessages: false, messages: action.res.data || null});

    case 'LOAD_MESSAGES_FAILURE':
        // also do something

    default:
      return state;
  }
}

As I'm using promiseMiddleware, LOAD_MESSAGES, LOAD_MESSAGES_REQUEST and LOAD_MESSAGES_FAILURE are dispacted as request /messages ends.

And now:

  1. Is it OK to dispatch loadMessages() in componentDidMount of MailListComponent?
  2. How should it be transitioned to /messages/1 properly?
  3. Should I create activeMessageId<Integer> in my state?
  4. How all these components should be connected with React-Router?

Here's my current tries:

export default (store) => {
  const loadAuth = (nextState, replaceState, next) => { ... };

  return (
    <Route name="app" component={App} path="/" onEnter={loadAuth}>
      <IndexRoute component={Content}/> // <== THIS IS A DUMMY COMPONENT. It diplays pre-loader until the app is transitioned to real first message
      <Route path="messages/:id" component={Message}/>
    </Route>
  );
};

Could you provide me some points, how to connect the dots? What is poper async data flow logic?

I'm using isomorphic-redux example as base for my app. Though is isomorphic, it shouldn't be too big difference between normal Redux app

Thank you.

UPDATE

One of the ideas — to set onEnter hook for <IndexRoute component={Content}/>, that will fetch messages, set into state and initialte transition. Is it redux+router way?

However, this way also may be rather tricky, 'cause /messages only works for authenticated users (where store.getState().auth.get('loaded') == true)

Best Answer

In my humble opinion, server-side rendering is important. Without it, you will be serving empty pages that only come to life on the client side. It will severely impact your SEO. So, if we think server-side rendering is important, we need a way to fetch data that fits in with server-side rendering.

Looking at the docs for server side rendering i.c.w. react-router, here is what we find:

  • First we call match, passing it the current location and our routes
  • Then we call ReactDOMServer.render, passing it the renderProps we got from match

It is clear that we need to have access to the fetched data before we proceed to the render phase.

This means we cannot use component lifecycle. Nor can we use onEnter or any other hook that only fires when render has already started. On the server side we need to fetch the data before render starts. Which means we need to be able to determine what to fetch from the renderProps we get from match.

The common solution is to put a static fetchData function on the top-level component. In your case it might look something like this:

export default class MailListComponent extends React.Component {
  static fetchData = (store, props) => {
    return store.dispatch(loadMessages());
  };
  // ....
}

We can find this fetchData function on the server-side and invoke it there before we proceed to render, because match gives us renderProps that contain the matched component classes. So we can just loop over them and grab all fetchData functions and call them. Something like this:

var fetchingComponents = renderProps.components
  // if you use react-redux, your components will be wrapped, unwrap them
  .map(component => component.WrappedComponent ? component.WrappedComponent : component)
  // now grab the fetchData functions from all (unwrapped) components that have it
  .filter(component => component.fetchData);

// Call the fetchData functions and collect the promises they return
var fetchPromises = fetchingComponents.map(component => component.fetchData(store, renderProps));

fetchData returns the result of store.dispatch, which will be a Promise. On the client side this will just show some loading screen until the Promise fulfills, but on the server side we will need to wait until that has happened so we actually have the data in the store when we proceed to the render phase. We can use Promise.all for that:

// From the components from the matched route, get the fetchData functions
Promise.all(fetchPromises)
  // Promise.all combines all the promises into one
  .then(() => {
    // now fetchData() has been run on every component in the route, and the
    // promises resolved, so we know the redux state is populated
    res.status(200);
    res.send('<!DOCTYPE html>\n' +
      ReactDOM.renderToString(
        <Html lang="en-US" store={app.store} {...renderProps} script="/assets/bridalapp-ui.js" />
      )
    );
    res.end();
})

There you go. We send a fully populated page to the client. There, we can use onEnter or lifecycle hooks or any other convenient method to get subsequent data needed when the user is navigating client-side. But we should try to make sure that we have a function or annotation (initial action?) available on the component itself so we can fetch data beforehand for the server-side render.