Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React router redux fetch resources if needed on child routes

I'm struggling to make this work, this is a common pattern I think but I haven't been able to see an example for this, or at a solution.

Here is the current route I am working on

/app/services/10/
  • in app fetch the current user’s login information
  • in /services fetches the list of services the user has available to them
  • in /10 fetch fine grained details of Service 10

So the way I do it to populate the store with some data is:

App

import Services from './routes/Services'

export default (store) => ({
  path: 'main',
  getComponent (nextState, cb) {
    require.ensure([], require => {
      const App        = require('./containers/AppContainer').default,
            userActions = require('../store/user').actions
      store.dispatch(userActions.fetch())

      cb(null, App)
    }, 'app')
  },
  childRoutes: [
    Services(store)
  ]
})

Services

Now the problem lies within the childRoutes:

import { injectReducer } from '../../../../store/reducers'
import Manage from './routes/Manage'

export default (store) => ({
  path: 'services',
  getComponent (nextState, cb) {
    require.ensure([], require => {
      const Services = require('./containers/ServicesContainer').default
      const actions      = require('./modules/services').actions
      const reducer      = require('./modules/services').default
      store.dispatch(actions.fetchAll())
      injectReducer(store, { key: 'services', reducer })
      cb(null, Services)
    })
  },
  childRoutes: [
    Manage(store)
  ]
})

As you can see the childRoute Services has a fetchAll() async request, that as you can imagine, needed some data from the store, specifically something from the user property in the store, like for example the userId or a token.

There wouldn't be a problem if I naturally navigate. But when I refresh, then the user prop hasn't been populated yet.

If you can't see how this is a problem, as part of my route:

app/services/10

The parameter 10 needed services from the store,

export default (store) => ({
  path: ':id',
  getComponent ({params: {id}}, cb) {
    require.ensure([], require => {
      const Manage              = require('./containers/ManageContainer').default
      const ServicesActions = require('../../modules/integrations').actions
      store.dispatch(ServicesActions.selectService(id))
      cb(null, Manage)
    })
  }
})

Where selectService is just a function that filters out state.services

The problem is services is fetched asynchronously and when you refresh that route, the store.dispatch gets executed even before the services in the store has completed and populated the store?

How do I approach this async issue?

like image 722
Joey Hipolito Avatar asked Dec 01 '22 12:12

Joey Hipolito


2 Answers

TL;DR : Use the lifecycle hook of your component to fetch data when they need it, and conditionally render a "loading" state if the props are not ready. Or use HoC to encapsulate this behavior in a more reusable way.

Your problem is interesting because it's not relevant only for react-router, but for any react / redux application that need data to be fetched before rendering. We all struggled at least once with this issue : "where do I fetch the data ? How do I know if the data are loaded, etc.". That's the problem frameworks like Relay try to address. One very interesting thing about Relay is that you can define some data dependencies for your components in order to let them render only when their data are "valid". Otherwise, a "loading" state is rendered.

We generally achieve a similar result by fetching the needed data in the componentDidMount lifecycle method and conditionally render a spinner if the props are not "valid" yet.

In your specific case, I I understand it correctly, it can be generalized like that :

  1. You hit the page /services/ with react-router
  2. Your ServicesContainer loads all the services
  3. You hit the page /services/10, since the services are already fetched there is no problem
  4. You now decide to refresh but the page is rendered before the async fetching has finished hence your issue.

As suggested by the other answer, you can tackle this issue by fetching the data if needed and not rendering the services until the data are fetched. Something like this :

class Services extends React.Component {

    componentDidMount() {
        if (!this.props.areServicesFetched) {
            this.props.fetchServices()
        }
    }
    
    render() {
        return this.props.areServicesFetched ? (
            <ul>
                {this.props.services.map(service => <Service key={service.id} {...service}/>)}
            </ul>
        ) : <p>{'Loading...'}</p>
    }

}

const ServicesContainer = connect(
    (state) => ({
        areServicesFetched: areServicesFetched(state)  // it's a selector, not shown in this example
        services: getServices(state)  // it's also a selector returning the services array or an empty array
    }),
    (dispatch) => ({
        fetchServices() {
            dispatch(fetchServices())  // let's say fetchServices is the async action that fetch services
        }
    })
)(Services)

const Service = ({ id, name }) => (
    <li>{name}</li>
)

That works great. You can stop reading this answer here if it's enough for you. If your want a better reusable way to do this, continue reading.

In this example, we are introducing some sort of "is my data valid to render or how can I make them valid otherwise ?" logic inside our component. What if we want to share this logic across different components ? As said by the doc :

In an ideal world, most of your components would be stateless functions because in the future we’ll also be able to make performance optimizations specific to these components by avoiding unnecessary checks and memory allocations. This is the recommended pattern, when possible.

What we can understand here is that all our components should be pure, and not taking care of the others component, nor of the data flow (by data flow I mean, "is my data fetched ?", etc.). So let's rewrite our example with only pure components without worrying about data fetching for now :

const Services = ({ services }) => (
    <ul>
        {services.map(service => <Service key={service.id} {...service}/>)}
    </ul>
)

Services.propTypes = {
    services: React.PropTypes.arrayOf(React.PropTypes.shape({
        id: React.PropTypes.string,
    }))
}


const Service = ({ id, name }) => (
    <li>{name}</li>
)

Service.propTypes = {
    id: React.PropTypes.string,
    name: React.PropTypes.string
}

Ok, so far we have our two pure components defining what props they need. That's it. Now, we need to put the "fetching data if needed when component did mount or render a loading state instead" somewhere. It's a perfect role for an Higher-Order Component or HoC.

Briefly speaking, an HoC lets you compose pure components together since they are nothing else than pure functions. An HoC is a function that takes a Component as an argument and return this Component wrapped with another one.

We want to keep separated the displaying of services and the logic to fetch them, because as I said earlier you may need the same logic of fetching the services in an another component. recompose is a little library that implements some very useful HoC for us. We're looking here at

  • lifecycle to add the componentDidMount lifecycle method
  • branch to apply a condition whether the services are fetched or not
  • renderComponent to render some <LoadingComponent> when services are fetching
  • mapProps to provide only the services prop to our <Services> component.
  • compose() utility to let us compose our HoC instead of nesting them

So let's build our ensureServices function which is responsible to :

  1. connect the pure component to the redux store
  2. Fetching the services if needed
  3. Rendering a loading state if services are not yet received from the server
  4. Rendering our component when the services are received

Here is an implementation :

const ensureServices = (PureComponent, LoadingComponent) => {
    
    /* below code is taken from recompose doc https://github.com/acdlite/recompose/blob/master/docs/API.md#rendercomponent */
    const identity = t => t

    // `hasLoaded()` is a function that returns whether or not the component
    // has all the props it needs
    const spinnerWhileLoading = hasLoaded =>
      branch(
        hasLoaded,
        identity, // Component => Component
        renderComponent(LoadingComponent) // <LoadingComponent> is a React component
      )

    /* end code taken from recompose doc */

    return connect(
        (state) => ({
            areAllServicesFetched: areAllServicesFetched(state),  // some selector...
            services: getServices(state)  //some selector
        }),
        (dispatch) => ({
            fetchServices: dispatch(fetchServices())
        })
    )(compose(
        lifecycle({
            componentDidMount() {
                if (!this.props.areAllServicesFetched) {
                    this.props.fetchServices()
                }
            }
        }),
        spinnerWhileLoading(props => props.areAllServicesFetched),
        mapProps(props => ({ services: props.services }))
    )(PureComponent))
}

Now, wherever a component need the services from the store, we can just use it like this :

const Loading = () => <p>Loading...</p>

const ServicesContainer = ensureServices(Services, Loading)

Here, our <Services> component just display the services but if you have for example a <ServicesForm> component that need services to render an input for each services, we could just write something like :

const ServicesFormContainer = ensureServices(ServicesForm, Loading)

If you wan't to generalize this pattern, you could take a look to react-redux-pledge, a tiny library I own that handles this kind of data dependencies.

like image 169
Pierre Criulanscy Avatar answered Dec 04 '22 01:12

Pierre Criulanscy


I've run into this quite a bit on the apps I've worked on. It seems like you're using React Router - if this is the case, you can take advantage of the onEnter/onChange hooks.

API Documentation is here: https://github.com/reactjs/react-router/blob/master/docs/API.md#onenternextstate-replace-callback

Instead of loading data in the async getComponent method, you can use the onEnter hook and use the callback parameter (just like you're doing with the getComponent) to indicate the react-router should block loading of this route until data is loaded.

Something like this could work, if you're using redux-thunk:

export default (store) => ({
  path: ':id',
  getComponent ({params: {id}}, cb) {
    require.ensure([], require => {
      const Manage              = require('./containers/ManageContainer').default
      const ServicesActions = require('../../modules/integrations').actions
      cb(null, Manage)
    })
  },
  onEnter: (nextState, replace, cb) => {
      const actions      = require('./modules/services').actions
      const reducer      = require('./modules/services').default
       //fetch async data
      store.dispatch(actions.fetchAll()).then(() => {
          //after you've got the data, fire selectService method (assuming it is synchronous)
          const ServicesActions = require('../../modules/integrations').actions
          store.dispatch(ServicesActions.selectService(id))
          cb()//this tells react-router we've loaded all data  
      })
  }
})

I've found the pattern of loading data using the router hooks to be a pretty clean way to ensure all of the data needed for the component to render is there. It's also a great way to intercept unauthenticated users, if necessary.

An alternative approach would be to explicitly load the data in the componentDidMount method of the component.

like image 31
dtraft Avatar answered Dec 04 '22 00:12

dtraft