Writing useEffect from scratch

joaquinniembro

Joaquin-Niembro

Posted on December 19, 2023

Writing useEffect from scratch

Polyfills are a good way to understand hooks better, even though they can seem challenging we should approach them as writing any custom hook, as the logic for writing any custom hook is the same for writing the hooks polypills for the most part!.

We can start of with what the useEffect hook is, we can break it down to a function that receives 2 things, an Effect which is nothing more that a function and a dependency array (or not in case we want it to execute on every render.

function useCustomUseEffect(fn, deps) {}
Enter fullscreen mode Exit fullscreen mode

Then we can define how many functionalities and edge cases this function needs to be able to handle.

  1. Execute the function on the first render.
  2. Find a way to know it is the first render.
  3. Cache the dependencies in order to compare after they have changed.
  4. Execute the effect again in case the dependencies have changed.
  5. Manage the cleanUp function and execute it after the effect.

Using refs for validating first render

function useCustomUseEffect(fn, deps) {
    const firstTimeRef = useRef(true);

    if (firstTimeRef.current) {
        // execute code first time and then set it to false as it will revalidate each render
        firstTimeRef.current = false; 
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we can go ahead and execute the effect on the first render, this way.

function useCustomUseEffect(fn, deps) {
  const firstTimeRef = useRef(true);

  if (firstTimeRef.current) {
    firstTimeRef.current = false;
    fn();
  }
}
Enter fullscreen mode Exit fullscreen mode

Now we need to determine a way to verify the dependecies and if the dependencies change through renders, this makes a special case for refs once again as they persist through renders.

Using refs to cache the dependency array

function useCustomUseEffect(fn, deps) {
  const firstTimeRef = useRef(true);
  const cacheDepsRef = useRef([]);

  if (firstTimeRef.current) {
    firstTimeRef.current = false;
    fn();
  }

  cacheDepsRef.current = deps ? deps : [];
}
Enter fullscreen mode Exit fullscreen mode

And now we can compare the deps that come from the argument with the cacheDepsRef.current!

we can now move on to re-executing the effect every time the dependencies change, in order to do that we can compare the reps as mentioned above and trigger the effect.

Execute effect when dependencies change

function useCustomUseEffect(fn, deps) {
  const firstTimeRef = useRef(true);
  const cacheDepsRef = useRef([]);

  if (firstTimeRef.current) {
    firstTimeRef.current = false;
    fn();
  }
  // comparing previous deps with current deps
  if (JSON.stringify(deps) === JSON.stringify(cacheDepsRef.current)) {
    fn();
  }

  cacheDepsRef.current = deps ? deps : [];
}
Enter fullscreen mode Exit fullscreen mode

we have only 2 more things left to do for our useEffect custom implementation.

We are missing the case when the deps array does not exist and the clean up function.

No dependencies are provided

For the case where we don't receive a dependency array we can simple add another validation to let it execute each render and we should be just fine.

function useCustomUseEffect(fn, deps) {
  const firstTimeRef = useRef(true);
  const cacheDepsRef = useRef([]);

  if (firstTimeRef.current) {
    firstTimeRef.current = false;
    fn();
  }

  if (JSON.stringify(deps) === JSON.stringify(cacheDepsRef.current)) {
    fn();
  }

  // execute on every render
  if (!deps) {
    fn();
  }
  cacheDepsRef.current = deps ? deps : [];
}
Enter fullscreen mode Exit fullscreen mode

And finally is time for cleaning up the effect!

Clean up function implementation

function useCustomUseEffect(fn, deps) {
  const firstTimeRef = useRef(true);
  const cacheDepsRef = useRef([]);

  if (firstTimeRef.current) {
    firstTimeRef.current = false;
    const cleanUp = fn();
    return () => {
      if (cleanUp && typeof cleanUp === "function") {
        cleanUp();
      }
    };
  }

  if (JSON.stringify(deps) === JSON.stringify(cacheDepsRef.current)) {
    const cleanUp = fn();
    return () => {
      if (cleanUp && typeof cleanUp === "function") {
        cleanUp();
      }
    };
  }

  if (!deps) {
    const cleanUp = fn();
    return () => {
      if (cleanUp && typeof cleanUp === "function") {
        cleanUp();
      }
    };
  }
  cacheDepsRef.current = deps ? deps : [];
}
Enter fullscreen mode Exit fullscreen mode

As easy as that! ok, now all of our edge cases are completed!πŸš€

So our custom useEffect is now completed! hopefully this is clear enough and anybody reading this is one step closer to being confident writing this custom implementations and adding more functionality on top!πŸ˜ƒ

πŸ’– πŸ’ͺ πŸ™… 🚩
joaquinniembro
Joaquin-Niembro

Posted on December 19, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related