Skip to content

Instantly share code, notes, and snippets.

@heygrady
Last active March 1, 2018 18:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save heygrady/5cd3de4048b92e36f73c629e0d33c87b to your computer and use it in GitHub Desktop.
Save heygrady/5cd3de4048b92e36f73c629e0d33c87b to your computer and use it in GitHub Desktop.
Loading data in a react-redux app.

Loading data in a react-redux app

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.

  1. URL contains an ID, it is matched by a route like <Route path="/detail/:id" component={View} />, which renders our <View />
  2. The <View /> component receives match in props. The match prop contains the ID in match.params.id.
  3. The <View /> component renders some <MyContainer id={match.params.id} /> component with an id prop.
  4. The <MyContainer id={match.params.id} /> container selects values from the state as props and passes them to the wrapped <My /> component.
  5. When <My /> component's lifecycle encounters componentWillMount or componentWillReceiveProps, it calls an onBeforeRender function (passed in from mapDispatchToProps by <MyContainer />)
  6. Then <MyContainer /> dispatches a thunk when onBeforeRender is called, which will check if the required values are in the state.
  7. If the values exist, then there is nothing more to do.
  8. 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.
  9. 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.

Examples

Given the URL: https://example.com/detail/123

Route

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

View

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

Container

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

Component

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

Loading component

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

Using a beforeRender higher-order-component

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

Rewriting the Component to use 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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment