Using useState inside custom react hooks causes renderings

Viewed 49

I created a custom react hook to fetch data. Unfortunately when the useGetData gets called from a component, the component will render for each useState that is performed inside the hook. How can I prevent the additional renderings?

export default function useGetData(
  setData: (fetchData) => void
): [(id: string) => void, boolean, boolean] {
  const [loadingData, setLoading] = useState(false)
  const [successData, setSuccess] = useState(false)

  const getData = (id: string) => {
    if (!id || !Number(id)) {
      setData(null)
      return
    }
    setSuccess(false)
    setLoading(true)
    Api.getData(Number(id))
      .then((response) => {
        setSuccess(true)
        setData(response)
      })
      .finally(() => {
        setLoading(false)
      })
  }

  return [getData, loadingData, successData]
}
1 Answers

React < 18.x does not do automatical batching for promise callbacks, timeouts, intervals, .... It does it only for event handlers registered via JSX props and in lifecycle methods / effects.

Solution 1: You can use "unstable" API ReactDOM.unstable_batchedUpdates which, despite being marked as unstable, has been working for years just fine:

    Api.getData(Number(id))
      .then((response) => ReactDOM.unstable_batchedUpdates(() => {
        setSuccess(true)
        setData(response)
      }))
      .finally(() => {
        setLoading(false)
      })

The function will batch all state updates that are executed inside of passed function (synchronous); in the example setSuccess and setData; so that that they will be merged into single re-render, just like React does it in onClick and other event handlers.

Other possibilities: I can think of 2 other options that I don't really like but might work for you:

  • You can merge all your state into single state variable so that you have single setter.

  • Instead of calling setSuccess(true); setData(response); in promise callback, you can do it in an effect by introducing another state variable.

const [response, setResponse] = useState();
useEffect(() => {
  if (!response) return;
  setSuccess(true);
  setData(response);
}, [response]);
...
Api.getData(Number(id))
  .then(setResponse)
  .finally(() => {
    setLoading(false)
  })
Related