[React] Safely setState on a Mounted React Component through the useEffect Hook

In the class version of this component, we had a method called safeSetState which would check whether the component was still mounted before trying to call setState. This is because our graphql client library is unable to cancel in-flight requests. Let’s make that same kind of thing work by tracking the mounted state of our component using the useRef and useEffect hooks.

 

We want a “lock”, which should run once when component inited, after component unmounted, it should be reseted.

We can use ‘useRef’ to build a container to hold our lock:

  const mountedRef = useRef(false);

Then we can use useEffect:

  useEffect(() => {
    mountedRef.current = true
    return () => (mountedRef.current = false)
  }, [])

The reason to use ‘[]’ as second arguement, is because we don’t want useEffect be triggered second times, we only want to run once, therefore, we use empty array, it won’t trigger scecond time.

 

Then we can create a safe setSetate function:

  const [state, setState] = useReducer(
    (state, newState) => ({...state, ...newState}),
    {
      loaded: false,
      fetching: false,
      data: null,
      error: null,
    },
  )

 const setSafeState = (...args) => mountedRef.current && setState(...args);

 

—-

Full code:

import {useContext, useReducer, useEffect, useRef} from 'react'
import PropTypes from 'prop-types'
import isEqual from 'lodash/isEqual'
import * as GitHub from '../../../github-client'

function useSetState(initialState) {
  return useReducer(
    (state, newState) => ({...state, ...newState}),
    initialState,
  )
}

function useSafeSetState(initialState) {
  const [state, setState] = useSetState(initialState)

  const mountedRef = useRef(false)
  useEffect(() => {
    mountedRef.current = true
    return () => (mountedRef.current = false)
  }, [])
  const safeSetState = (...args) => mountedRef.current && setState(...args)

  return [state, safeSetState]
}

function Query({query, variables, normalize = data => data, children}) {
  const client = useContext(GitHub.Context)
  const [state, setState] = useSafeSetState({
    loaded: false,
    fetching: false,
    data: null,
    error: null,
  })

  useEffect(() => {
    if (isEqual(previousInputs.current, [query, variables])) {
      return
    }
    setState({fetching: true})
    client
      .request(query, variables)
      .then(res =>
        setState({
          data: normalize(res),
          error: null,
          loaded: true,
          fetching: false,
        }),
      )
      .catch(error =>
        setState({
          error,
          data: null,
          loaded: false,
          fetching: false,
        }),
      )
  })

  const previousInputs = useRef()
  useEffect(() => {
    previousInputs.current = [query, variables]
  })

  return children(state)
}

Query.propTypes = {
  query: PropTypes.string.isRequired,
  variables: PropTypes.object,
  children: PropTypes.func.isRequired,
  normalize: PropTypes.func,
}

export default Query

 

Related Posts

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注