An Important Lesson in React Hooks

Today I learned something important about reaction hooks, and to celebrate my #noonies2022 nomination, I decided to write a short article explaining my findings.

React brackets are regular functions.

When you create a new useSomething hook, you are creating a new javascript function. This may seem routine to some of you. However, you might forget the basics after using custom hooks from popular libraries like redux, rea query, mobx, etc.

So let me explain my thinking today.

Today’s goal was to create a data layer to synchronize server data with client state. So I naturally decided to create a custom hook to encapsulate the logic and make it easier to use the data layer in the application.

My initial code looks like this:

export const useData = (initalStatus) => {
  const [data, setData] = useState(initalStatus);

  return {
    data,
    setData,
  };
};

The component structure I had in mind looked like this:

Because StepOne and StepTwo are children of the application, and according to the diagram I created, I thought the data state would be the same for all three components due to the React magic. Is not it?

The answer is no. Each time you call a hook within the scope of a component, useState creates a new instance of the component-level object. So there is no connection between the state of the components other than using the same function to create it.

So how can I create a single data layer for the whole component family?

First, I tried to solve the problem using basic javascript knowledge. My mental process was: Create a global variable at the custom hook level. Then use a useEffect to sync the outer object with the inner state to force a re-render at the consumer level. Finally, I expose the data by flipping it.

The custom hook with the manual cache implementation looked like this:

let dataCache;
const updateCache = (newData) => {
  dataCache = newData;
};
export const useData = (initalStatus) => {
  const [data, setData] = useState(initalStatus);

  useEffect(() => {
    if (!dataCache || dataCache !== data) {
      updateCache(data);
    }
  }, [data]);

  return {
    data,
    dataCache,
    setData,
    updateCache: (newData) => {
      updateCache(newData);
      setData(newData);
    },
  };
};

Can you tell me what is the limit of my solution? Because I’m using an object outside the scope of the react library, I can’t start a new render on the parent components. So, we have to remember that internal state will trigger a re-render in the current element and its children, but not in the grandparent. So the solution will work as long as all components that need access to the data layer are ancestors. Additionally, it is read-only for consumers, and data mutation is limited to the first element in the DOM tree that uses the square bracket.

Is it possible to create a custom hook that forces a re-render in each component when needed?

I started researching how to create something like a singleton custom hook in React. It didn’t take me long to find two libraries that caught my attention. First, I found “React singleton hook” with 141 stars on Github, and a Reddit thread suggested I try “Use between” with 181 stars. Luckily, both solved my problem rendering the parent and allowed me to use a custom hook as a React context.

I don’t want to dive into the implementation details of the two libraries. However, from a general perspective, the React singleton hook is the code I was trying to generate with a global variable, which includes rendering the parent component from the children. In contrast, the use-between library uses an observer pattern to solve the problem.

I think it’s a good idea if someone writes a blog post explaining the Javascript pattern behind these libraries. I love reading it!

If you want to play with the code I created for this article; I created this instance of Stackblitz with my findings.

How can I create a data layer with the tools available from React?

Finally, after all my research, I solved the problem using one of the well-documented React tools. I realized what I was trying to create was the React Context.

Again, this article is not meant to explain any React concept in depth but to show you my thought process. Nevertheless, The React Context, in a nutshell, uses the createContext function, which creates the provider and consumer reference. Then the provider handles the re-rendering of the components, and the reference + useContext hook exposes the desired data to the consumer.

Once I implemented the React Context, everything started working as expected, and right after I was done, I decided to share my knowledge with this blog post.

I hope you learned something new or that this lecture allowed you to refresh your memory on some basic knowledge of javascript. Sometimes taking a moment to re-evaluate your solution helps deliver quality, well-designed, and robust code in a reasonable amount of time.

Happy coding everyone. Don’t forget to vote for me at #noonies2022, and as I always say Good Science! Before you finish reading, and if you are Spanish-speaking, consider listening to my podcast #programadoresAnónimos 😀.

LOADING
. . . comments & After!

Comments are closed.