f1nn
f1nn

Reputation: 7047

Async data flow in React App with Redux + ReactRouter?

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)

Upvotes: 21

Views: 5288

Answers (3)

ndreckshage
ndreckshage

Reputation: 873

The React-Router 2 API opens up interesting data fetching possibilities, by letting you have a layer between the Router and your Application. It's a great place for an additional plugin like https://github.com/rackt/async-props or https://github.com/raisemarketplace/ground-control .

The universal fetchData hook in GroundControl (Redux specific) gives you quite a bit of power. Lets you block component render client side; handle render async; and then dispatch to your reducer.

const fetchData = (done, { dispatch, clientRender }) => {
  // show blocking loading component
  setTimeout(() => {
    clientRender(); // non-blocking loader (props.loading)
    setTimeout(() => {
      dispatch(actions.load({ mydata });
      done();
    }, 1000);
  }, 1000)
};

Reducers are declared on routes and the fetchData function for each nested route is called. So, you can have parent layout route handle auth. Then fetchData in nested routes handle what they care about.

Upvotes: 0

Stijn de Witt
Stijn de Witt

Reputation: 42075

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.

Upvotes: 17

Erik Aybar
Erik Aybar

Reputation: 4791

I have been working on a fairly large app (React, Redux, React Router, etc...) with a very similar feature (message browser w/ sidebar + search bar/tools, etc...) It is almost identical structurally to what you've laid out above. Making use of React's component lifecycle has has worked out very well for us.

Basically leaving it up to the component to decide, "Given this data (messages, loading, etc...), what should I look like and/or do?".

We began by messing with onEnter and other "outside of the component" strategies, but they began to feel overly complex. Also related is your question about storing activeMessageId. If I understand your scenario correctly, this should be reliably derived from your current route params.id in the example.

To give an idea of some things this approach is accomplishing for us

enter image description here

Of course this example is stripped down/simplified quite a bit, but it summarizes the "request messages" portion and is very close to the actual approach that is working for us.

const MailApp = React.createClass({
  componentWillMount() {
    this._requestIfNeeded(this.props);
  },

  componentWillUpdate(newProps) {
    this._requestIfNeeded(newProps);
  },

  _requestIfNeeded(props) {
    const {
      // Currently loaded messages
      messages,

      // From our route "messages/:id" (ReactRouter)
      params: {id},

      // These are our action creators passed down from `connect`
      requestMessage,
      requestMessages,

      // Are we "loading"?
      loading,
      } = props;

    if (!id) {
      // "messages/"
      if (messages.length === 0 && !loading)
        return requestMessages();
    } else {
      // "messages/:id"
      const haveMessage = _.find(messages, {id});
      if (!haveMessage && !loading)
        return requestMessage(id);
    }
  },

  render() {
    const {
      messages,
      params: {id},
    } = props;

    return (
      <div>
        <NavBar />
        <Message message={_.find(messages, {id})}/>
        <MailList message={messages} />
      </div>
    )
  }
});

I would love to hear if this helps you or if you land elsewhere. I've seen similar questions crop up around these topics and would be interested in what you find out.

Upvotes: 4

Related Questions