Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Async data flow in React App with Redux + ReactRouter?

Tags:

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)

like image 280
f1nn Avatar asked Dec 21 '15 09:12

f1nn


People also ask

Can I use Redux with react router?

You can use the connected-react-router library (formerly known as react-router-redux ). Their Github Repo details the steps for the integration. Once the setup is complete, you can now access the router state directly within Redux as well as dispatch actions to modify the router state within Redux actions.

Is Redux dispatch synchronous or asynchronous?

Introduction. By default, Redux's actions are dispatched synchronously, which is a problem for any non-trivial app that needs to communicate with an external API or perform side effects.

How data flows and the application state changes in a Redux application?

Redux uses a "one-way data flow" app structure When something happens in the app: The UI dispatches an action. The store runs the reducers, and the state is updated based on what occurred. The store notifies the UI that the state has changed.

How does data flow in Redux architecture?

Redux follows the unidirectional data flow. It means that your application data will follow in one-way binding data flow. As the application grows & becomes complex, it is hard to reproduce issues and add new features if you have no control over the state of your application.


2 Answers

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.

like image 60
Stijn de Witt Avatar answered Oct 13 '22 17:10

Stijn de Witt


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.

like image 33
Erik Aybar Avatar answered Oct 13 '22 16:10

Erik Aybar