Made in Builder.io

Watch the biggest Figma-to-code launch of the year

Builder.io logo
Talk to Us
Platform
Developers
Talk to Us

Blog

Home

Resources

Blog

Forum

Github

Login

Signup

×

Visual CMS

Drag-and-drop visual editor and headless CMS for any tech stack

Theme Studio for Shopify

Build and optimize your Shopify-hosted storefront, no coding required

Resources

Blog

Get StartedLogin

‹ Back to blog

Web Development

Why useEffect May Not Be the Best Abstraction

February 23, 2023

Written By Miško Hevery

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

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:

  1. Way to re-execute some code repeatedly.
  2. 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.

A screenshot of the difference between SSR/SSG execution models in React and Qwik.

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:

A screenshot showing SSR/SSG in React rendering blank content.

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:

A screenshot of SSR/SSG correctly rendering content with Qwik useTask.

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.

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.

Screenshot of Qwik code next to React code.

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.

Screenshot of React code on top, Qwik code on bottom.

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.

We discussed the limitations of useEffect() and highlighted four problems:

  1. It only runs on the client.
  2. It always runs after render.
  3. It has a fixed set of dependency trackers.
  4. 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: a new AI model to convert Figma designs to high quality code in a click.

No setup needed. 100% free. Supports all popular frameworks.

Try Visual Copilot

Share

Twitter
LinkedIn
Facebook
Hand written text that says "A drag and drop headless CMS?"

Introducing Visual Copilot:

A new AI model to turn Figma designs to high quality code.

Try Visual Copilot
Newsletter

Like our content?

Join Our Newsletter

Continue Reading
Visual Copilot5 MIN
Announcing Visual Copilot 1.0: General Availability, Bring Your Own Components, And More
WRITTEN BYSteve Sewell
March 13, 2024
Web Development8 MIN
How we built the dynamic tickets feature for Builder Accelerate
WRITTEN BYVishwas Gopinath
March 6, 2024
Web Development20 MIN
Why React Server Components Are Breaking Builds to Win Tomorrow
WRITTEN BYVishwas Gopinath
February 28, 2024