useEffect()
is a fundamental hook in the React ecosystem. Let’s look at some use cases where the useEffect()
is not the best abstraction and look into possible different ways of thinking about it.
Too rudimentary
There are a number of reasons that useEffect()
is too limited, which include:
- Only running on the client
- Always running after the render
- Having a fixed set of dependency trackers
- Requiring dirty checking of the dependency array
It only runs on the client
useEffect()
is never executed as part of the server rendering. This is an important distinction because we need to have a way to separate code that we can run on both server and client from the code which can execute only on the client (DOM access.)
The issue is that useEffect()
is really two APIs in one. The useEffect()
API provides:
- Way to re-execute some code repeatedly.
- Way to conditionally execute code on the client.
The challenge is that those two aspects of the useEffect()
should be controlled separately. Sometimes I want to run code repeatedly, and sometimes I only want to run it on the client. The two aspects should not be conflated.
Let’s look at an example of using useEffect()
to load data during SSR/SSG:
function WeatherWidget(props: { zipcode: string }) {
const [zipcode, setZipcode] = useState(props.zipcode);
const [weather, setWether] = useState('');
useEffect(() => {
fetchWeather(zipcode).then((weather) => setWether(weather));
}, [zipcode]);
return (
<div>
<h1>Weather</h1>
<input
placeholder="zipcode"
value={zipcode}
onInput={(e) => setZipcode((e.target as HTMLInputElement).value)}
/>
<div>Weather: {weather}</div>
</div>
);
}
During SSR/SSG this results in a UI like the following because SSR/SSG does not execute the useEffect()
code:
Now let’s look at an alternative framework, Qwik, which distinguishes between the client/server versus running something multiple times. Notice that we call it useTask$()
to imply that it can execute multiple times.
export default component$(({ zipcode }: { zipcode: string }) => {
const store = useStore({
zipcode: zipcode,
weather: "",
});
useTask$(async ({ track }) => {
track(() => store.zipcode);
store.weather = await fetchWeather(store.zipcode);
});
return (
<div>
<h1>Weather</h1>
<input
placeholder="zipcode"
value={zipcode}
onInput$={(e) => (store.zipcode = (e.target as HTMLInputElement).value)}
/>
<div>Weather: {store.weather}</div>
</div>
);
});
During SSR/SSG this will result in the correct UI because SSR/SSG execute the useTask$()
API:
It always runs after the render
There is another limitation of useEffect()
. It can only run after rendering. Again this is because useEffect()
combines two behaviors. Where to run (client/server) and when to run (after rendering);
The issue with the above example is even if useEffect()
would run a server as part of SSR/SSG; it would run too late. It would execute after the UI has been rendered (and possibly streamed to the client.) What is needed is a way to run code before the rendering completes. This is why Qwik splits the useEffect()
into useTask$()
and useVisibleTask$()
.
useTask$()
runs before rendering and it blocks rendering. See our example:
useTask$(async ({ track }) => {
track(() => store.zipcode);
store.weather = await fetchWeather(store.zipcode);
});
Notice that the useTask$()
function is async
and contains await
. The rendering of the UI needs to be held until after the useTask$()
completes, as the useTask$()
is responsible for fetching the data.
It has a fixed set of dependency trackers
The useEffect()
can re-execute the function whenever its inputs change. This is an important behavior for code. The issue is that the set of dependencies is fixed at compile time. There are cases when it is convenient to change the dependencies, which triggers rerunning the effect.
Dirty checking of the deps array
Finally, the useEffect()
dependency array must constantly re-execute to determine if things have changed. This is problematic because it forces eager execution and download of code.
In this Qwik example, the task function does not need to execute (nor download) until the tracking signal fires. This greatly limits the amount of code that needs to be eagerly available to the client.
Conclusion
We discussed the limitations of useEffect()
and highlighted four problems:
- It only runs on the client.
- It always runs after render.
- It has a fixed set of dependency trackers.
- It requires dirty checking of the dependency array to trigger the effect.
We think that useEffect()
is too elementary because it conflates where to run (client/server) with when to run (before/after) rendering. An alternative framework, Qwik, separates where and when APIs into two separate APIs useTask$()
and useBrowserVisibleTask$()
to give the developer more control.
Introducing Visual Copilot: convert Figma designs to high quality code in a single click.