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/
app
fetch the current user’s login information/services
fetches the list of services the user has available to them/10
fetch fine grained details of Service 10So the way I do it to populate the store with some data is:
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)
]
})
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?
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 :
/services/
with react-routerServicesContainer
loads all the services/services/10
, since the services are already fetched there is no problemAs 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
componentDidMount
lifecycle method<LoadingComponent>
when services are fetchingservices
prop to our <Services>
component.So let's build our ensureServices
function which is responsible to :
connect
the pure component to the redux storeservices
if neededservices
are not yet received from the serverservices
are receivedHere 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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With