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.