Skip to content

Instantly share code, notes, and snippets.

@AZaviruha
Last active September 2, 2021 06:30
Show Gist options
  • Save AZaviruha/1d4702c5c5a190bbdbd1091d8aaaab93 to your computer and use it in GitHub Desktop.
Save AZaviruha/1d4702c5c5a190bbdbd1091d8aaaab93 to your computer and use it in GitHub Desktop.

It is possible to implement side-effects in the React component without any additional tool, like Redux's middlewares (saga, thunks, etc) . It can be achieved using the combination of useEffect and async\await functions, maybe wrapped inside useCallback This approach is workable, but it has some pitfalls.

Let's look at this example:

function MyComponent () {
  const [x, setX] = useState(42)
  const [y, setY] = useState(43)
  const [foo, setFoo] = useState(null)
  
  const getData = useCallback(async (x, y) => {
    const data = await loadData({ x, y })
    setFoo(data)
  }, [])
  
  useEffect(() => {
     getData(x, y)
  }, [x, y])
}

This all works well, until x and y is enough for getData to fetch data. But what if we need some additional argument (z) and at the same time we don't want to use this argument as a dependency in the useEffect (because changes of z can trigger unnecessary calls of this effect)?

function MyComponent () {
  const [x, setX] = useState(42)
  const [y, setY] = useState(43)
  const [z, setZ] = useState(44)
  const [foo, setFoo] = useState(null)
  
  const getData = useCallback(async (x, y) => {
    const data = await loadData({ x, y, z })
    setFoo(data)
  }, [ /* z ?? */])
  
  useEffect(() => {
     getData(x, y)
  }, [x, y,  /* z ?? */])
}

We can't add z as a dependency to the useCallack too, because in that case we will need to add the getData function itself to the effect's dependencies. Without this we have a chance to call an outdated getData (with old value of z inside it). But this is not what we want. We want only x and y to be the reason of useEffect calls.

For now I discovered only one solution for this problem: refs.

function MyComponent () {
  const [x, setX] = useState(42)
  const [y, setY] = useState(43)
  const [foo, setFoo] = useState(null)
  const z = useRef(null)
  
  const getData = useCallback(async (x, y) => {
    const data = await loadData({ x, y, z: z.current })
    setFoo(data)
  }, [])
  
  useEffect(() => {
     getData(x, y)
  }, [x, y])
}

But to be honest, this looks more like a hack than a "proper solution". It becomes obvious when you need to use z as a property for some component: it will not work, because change of the ref does not trigger re-rendering of the component. So you can't do this:

function MyComponent () {
  const [x, setX] = useState(42)
  const [y, setY] = useState(43)
  const [foo, setFoo] = useState(null)
  const z = useRef(null)
  
  const getData = useCallback(async (x, y) => {
    const data = await loadData({ x, y, z: z.current })
    setFoo(data)
  }, [])
  
  const handleOnClick = useCallback(() => {
    z.current = Date.now() // Will not trigger re-render
  }, [])
  
  useEffect(() => {
     getData(x, y)
  }, [x, y])
  
  return (
    <div>
      <button onClick={handleOnClick}>Update "z"</button>
      <AnotherComponent value={z.current} />
    </div>
  )
}

You can solve this problem using another "dirty hack":

function MyComponent () {
  const [x, setX] = useState(42)
  const [y, setY] = useState(43)
  const [foo, setFoo] = useState(null)
  const z = useRef(null)
  const [, forceRender] = useState(null)
  
  const getData = useCallback(async (x, y) => {
    const data = await loadData({ x, y, z: z.current })
    setFoo(data)
  }, [])
  
  const handleOnClick = useCallback(() => {
    z.current = Date.now() // Will not trigger re-render
    forceRender() // but this will
  }, [])
  
  useEffect(() => {
     getData(x, y)
  }, [x, y])
  
  return (
    <div>
      <button onClick={handleOnClick}>Update "z"</button>
      <AnotherComponent value={z.current} />
    </div>
  )
}

Using a meaningless state setter we can force re-render, but at this moment your code will look like a combination of dirty hacks and workarounds. I don't like how it smells.

It seems that right now using Redux with some middleware provides more straight-forward solution to the side-effect management. I should to admit it even despite the fact that I'm a big fan of using React without additional libraries.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment