Decoding React Hooks

From useState to useEffect: Demystifying React Hooks’ Core Operations

Danielle Dias
Geek Culture
4 min readAug 13, 2023

--

Photo by Trophy Technology on Unsplash

A Primer on React Hooks

To begin with, let’s take a brief moment to remind ourselves what React hooks are. Introduced in React 16.8, hooks are a set of APIs that allow us to leverage stateful logic and lifecycle methods directly inside our functional components, without needing to resort to class-based components. Hooks have become extremely popular due to their ability to simplify code structure and improve code reuse.

The two most common hooks you’re likely to encounter are useState and useEffect. The useState hook allows you to add state to your functional components, and the useEffect hook lets you perform side effects, like fetching data or manipulating the DOM directly, akin to the lifecycle methods in class-based components.

Under the Hood of useState Hook

But what happens when we invoke these hooks? How does React keep track of all these state variables and effects? Let’s start by inspecting the useState hook.

When you call the useState hook, it returns an array with two elements. The first element is the current state value, and the second is a function that allows us to update that state. You might use it like this:

const [count, setCount] = useState(0);

Here, count is the current state (initialized to 0), and setCount is the function we use to update the count.

But, how does React manage these state variables, especially when there are multiple useState calls in a single component? The answer lies in the order of the hooks' invocation. React maintains an internal list of hooks for each component. Each time a hook is called, React records it in this list, and the order is crucial.

On subsequent re-renders of the component, React relies on the hooks being called in the exact same order. It’s the reason why you’ll find in the React documentation that hooks should never be called conditionally or within loops. Violating this order could lead to bugs and inconsistencies in your application.

Here’s a simplified code example to illustrate how React might be managing this:

let hooks = []; // This array will hold our states
let currentHook = 0; // A pointer to the current hook

function useState(initialValue) {
hooks[currentHook] = hooks[currentHook] || initialValue; // If state doesn't exist, initialize it
let hookIndex = currentHook; // Store the current hook index
const setState = (newState) => {
hooks[hookIndex] = newState; // Update the state
};
return [hooks[currentHook++], setState]; // Return current state and updater function, then move the pointer
}

The Magic of useEffect Hook

Now that we’ve demystified the workings of useState, let's shift our focus to the useEffect hook.

The useEffect hook serves the purpose of managing side-effects in your components. Side-effects could be anything from fetching data, subscribing to events, manual DOM manipulations, and so forth. It can also clean up these side-effects when the component unmounts or before running the effect again.

The useEffect hook takes two parameters: a callback function that containsthe side effect logic, and an optional dependency array. If the dependency array is provided, the effect runs only when the dependencies have changed between renders.

useEffect(() => {
console.log('The count has changed!');
}, [count]);

In the example above, the console log statement (our side effect) will run only when the count value changes.

But how does React manage this behind the scenes? It again relies on the hooks array. For each useEffect hook, React stores not just the effect function but also its dependencies. When a component re-renders, React compares the current dependencies with the previous ones. If they are different, it runs the effect.

Here’s a simplified illustration of how this might look in code:

function useEffect(callback, dependencies) {
const hook = hooks[currentHook];
const hasNoDependencies = !dependencies;
const dependenciesChanged = hook ? !dependencies.every((dep, i) => dep === hook.dependencies[i]) : true;
if (hasNoDependencies || dependenciesChanged) {
callback();
hooks[currentHook] = { callback, dependencies };
}
currentHook++; // Move the pointer
}

In this code, we’re storing the callback and dependencies for each useEffect hook. On subsequent renders, we check if the dependencies have changed, and if so, we run the callback function.

Remember, the real implementation within React is significantly more complex, optimized for performance, and handles edge cases gracefully. This simplified version is just to give us a conceptual understanding.

Conclusion

To summarize, React hooks have revolutionized the way we write and manage stateful logic in our React components, making them more readable, reusable, and maintainable. Under the hood, React relies on a simple yet ingenious system of an ordered list of hooks for each component, allowing it to keep track of all the state variables and effects.

Remember, hooks should always be used at the top level of your React functions. They should not be used in loops, conditions, or nested functions to ensure that the order of the hook calls is the same between multiple renders.

Thanks for reading. If you enjoyed this article, consider supporting me by becoming a Medium member.

--

--