This document covers the details of a component that needs to fetch required data before rendering. In our imaginary app we're using react, redux, and react-router. We're covering the specific use-case of a detail page that gets its id
from the URL.
- URL contains an ID, it is matched by a route like
<Route path="/detail/:id" component={View} />
, which renders our<View />
- The
<View />
component receivesmatch
in props. Thematch
prop contains the ID inmatch.params.id
. - The
<View />
component renders some<MyContainer id={match.params.id} />
component with anid
prop. - The
<MyContainer id={match.params.id} />
container selects values from the state as props and passes them to the wrapped<My />
component. - When
<My />
component's lifecycle encounterscomponentWillMount
orcomponentWillReceiveProps
, it calls anonBeforeRender
function (passed in frommapDispatchToProps
by<MyContainer />
) - Then
<MyContainer />
dispatches a thunk whenonBeforeRender
is called, which will check if the required values are in the state. - If the values exist, then there is nothing more to do.
- If the values do not exist, the dispatched thunk needs to set a loading state, request the missing data, store the data in the state and unset the loading state.
- All of these state changes trigger the
<My />
component to show a loading screen until the data has finished loading and then re-renders to show the final rendered component.
Note: this example won't work seamlessly in a server-rendered app. For server-side rendering you need to pre-fill your state with all of your required data. Preloading state on the server is outside the scope of this document. However, if you have a preloaded state, the above sequence will indeed work on the server, correctly rendering the final component in step 7.
Given the URL: https://example.com/detail/123
Somewhere in the <App />
we need to render a route. React-router will match this path against the URL and conditionally render the <View />
component.
Note: how you render your app is outside the scope of this document.
import React from 'react'
import { Route } from 'react-router-dom'
import View from './View'
const App => () => (
<div>
<Route path="/detail/:id" component={View} />
</div>
)
export default App
Our <View />
component will have history
, location
and match
injected just like it was wrapped in withRouter
. For our demonstration here we only need the match
prop. We add the id
as a prop on the <MyContainer />
. This is how our container will know which data to read from the redux state.
import React from 'react'
import PropTypes from 'prop-types'
import MyContainer from './MyContainer'
const View => ({ match }) => (
<div>
<MyContainer id={match.params.id} />
</div>
)
View.propTypes = {
match: PropTypes.object.isRequired
}
export default View
The container component uses react-redux connect
to inject props into the final component. We can use the id
prop to read values from the state and to request the data that the component needs. From within a container we don't have access to the react lifecycle methods so there's no obvious way to initiate the onBeforeRender
method. We need to call onBeforeRender
from within the <My />
component since it will be able to call the function when componentWillMount
and componentWillReceiveProps
.
You can see that we're reading a number of values from the state using selectors. If the curried selectors look strange, you can read more about configurable selectors. Our component doesn't need the loaded
prop just yet; we'll be using that later on.
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import My from './My'
import { selectById, selectError, selectIsLoading } from './modules/detail/selectors'
import { beginLoading, endLoading, receiveError, receiveData } from './modules/detail/actions'
const mapStateToProps = (state, ownProps) => {
const { id } = ownProps
const detail = selectById(id)(state)
const error = selectError(id)(state)
const loading = selectIsLoading(id)(state)
return {
detail,
error,
loaded: !!detail,
loading
}
}
const mapDispatchToProps = (dispatch, ownProps) => {
const { id } = ownProps
return {
// dispatch an inline thunk
onBeforeRender: () => dispatch((_, getState) => {
const state = getState()
const detail = selectById(id)(state)
const loading = selectIsLoading(id)(state)
// missing data; not already loading it
if (!detail && !loading) {
// tell the app we're loading it
dispatch(beginLoading(id))
// begin loading the data
fetch(`https//api.example.com/detail/${id}`)
.then(result => result.json())
// receive the data or an error
.then(data => dispatch(receiveData({ id, data })))
.catch(error => dispatch(receiveError({ id, error })))
// end loading the data (regardless of error)
.finally(() => endLoading(id))
}
})
}
}
const MyContainer = connect(mapStateToProps, mapDispatchToProps)(My)
export default MyContainer
Inside the component we use react component lifecycle methods to trigger the loading of missing data. Before our component is mounted and whenever it receives new props we need to call onBeforeRender
to ensure we have the data our component needs to render.
You may want to model your loading component after the example in react-loadable. If you are familiar with that interface you may notice that the implementation shown here is naive and skips the notion of timedOut
and pastDelay
. We'll be able to fix that using a higher-order comonent in the next step.
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import Loading from './Loading'
class My extends Component {
componentWillMount() {
const { detail, loading } = this.props
if (!detail && !loading) {
onBeforeRender()
}
}
componentWillReceiveProps(nextProps) {
const { detail, loading } = nextProps
if (!detail && !loading) {
onBeforeRender()
}
}
render() {
const { detail, loading, error } = this.props
if (loading) {
return (<Loading error={error} pastDelay />)
}
return (
<div>
<h2>{detail.title}</h2>
<p>{detail.description}</p>
</div>
)
}
}
My.propTypes = {
detail: PropTypes.object,
error: PropTypes.bool,
loading: PropTypes.bool,
onBeforeRender: PropTypes.func.isRequired
}
export default My
The loading component interface defined by react-loadable is very robust and allows us to have tight control over when the loading component renders. The example above is naive and skips the implementation of timeouts to support the pastDelay
and timedOut
props.
import React from 'react'
import PropTypes from 'prop-types'
const Loading = ({ error, timedOut, pastDelay }) => {
if (error) {
return <div>Error!</div>
} else if (timedOut) {
return <div>Taking a long time...</div>
} else if (pastDelay) {
return <div>Loading...</div>
} else {
return null
}
}
Loading.propTypes = {
error: PropTypes.bool,
timedOut: PropTypes.bool,
pastDelay: PropTypes.bool
}
export default Loading
Below you can see a (probably) complete higher order component that we can use to wrap our <My />
component to manage the loading lifecycle for us. It has been designed to work with the container design illustrated above. It implements loading timers as well to help with loading screen flashing and timing out. It requires us to set a loaded
boolean (we already are) to indicate when the necessary data has been loaded. Using a higher order component allows us to apply this pattern to any component that needs to load data before it renders.
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import hoistStatics from 'hoist-non-react-statics'
const beforeRender = (Loading, { delay = 200, timeout = 2000 } = {}) => (WrappedComponent) => {
class BeforeRender extends Component {
constructor(props, context) {
super(props, context)
this.state = {
pastDelay: false,
timedOut: false,
}
}
componentWillMount() {
const { error, loading, loaded, onBeforeRender } = this.props
if (!loaded && !loading && !error) {
this.startTimeouts()
onBeforeRender()
} else if (loaded || error) {
this.clearTimeouts()
}
}
componentWillReceiveProps(nextProps) {
const { error, loading, loaded, onBeforeRender } = nextProps
if (!loaded && !loading && !error) {
this.startTimeouts()
onBeforeRender()
} else if (loaded || error) {
this.clearTimeouts()
}
}
componentWillUnmount() {
this.clearTimeouts(true)
}
startTimeouts() {
this.setState({ pastDelay: false, timedOut: false })
this.clearTimeouts()
this.delayId = setTimeout(() => {
this.delayId = undefined
this.setState({ pastDelay: true })
}, delay)
this.timeoutId = setTimeout(() => {
this.timeoutId = undefined
this.setState({ timedOut: true })
}, timeout)
}
clearTimeouts(unmounting) {
if (this.delayId) {
clearTimeout(this.delayId)
if (!unmounting) { this.setState({ pastDelay: false }) }
this.delayId = undefined
}
if (this.timeoutId) {
clearTimeout(this.timeoutId)
if (!unmounting) { this.setState({ timedOut: false }) }
this.timeoutId = undefined
}
}
render() {
const { pastDelay, timedOut } = this.state
const { error, loading } = this.props
if (loading || error) {
return (
<Loading error={error} pastDelay={pastDelay} timedOut={timedOut} />
)
}
const { wrappedComponentRef, ...remainingProps } = this.props
return <InnerComponent {...remainingProps} ref={wrappedComponentRef} />
}
}
BeforeRender.propTypes = {
error: PropTypes.bool,
loaded: PropTypes.bool,
loading: PropTypes.bool,
onBeforeRender: PropTypes.func.isRequired,
wrappedComponentRef: PropTypes.func
}
BeforeRender.displayName = `beforeRender(${InnerComponent.displayName || InnerComponent.name})`
BeforeRender.WrappedComponent = InnerComponent
hoistStatics(BeforeRender, InnerComponent)
return BeforeRender
}
export default beforeRender
Now we can safely remove the lifecycle methods from our component and convert it to a stateless functional component. Because the beforeRender
component handles the loading process for us, our component only needs to concern itself with rendering the data.
import React from 'react'
import PropTypes from 'prop-types'
import Loading from './Loading'
import beforeRender from './beforeRender'
const My = ({ detail }) => {
return (
<div>
<h2>{detail.title}</h2>
<p>{detail.description}</p>
</div>
)
}
My.propTypes = {
detail: PropTypes.object
}
export default beforeRender(Loading)(My)