Introduction
Welcome to your next lesson! In our previous discussion on controlled vs. uncontrolled components, we saw how controlled components, while offering great predictability, can lead to repetitive logic. For instance, multiple form fields might each require a state variable and an onChange handler, leading to duplicated code.
This lesson directly addresses that challenge. We'll explore one of the most powerful features in modern React: Custom Hooks. Our goal is to implement a custom hook to encapsulate and reuse stateful logic between components.
You've expressed a strong interest in the "why" and the mechanics behind React's features, and custom hooks are a perfect topic for this. They aren't just a convenient pattern; they are a natural extension of React's core design. We will peel back the layers to understand how they are made possible by fundamental JavaScript concepts like closures and how they fit into React's internal rendering model.
By the end of this lesson, you will be able to:
- Define what a custom hook is and when to create one.
- Implement your own custom hooks to abstract away complex or repetitive logic.
- Explain the internal mechanism that makes hooks work, including the "Rules of Hooks."
- Distinguish between sharing stateful logic and sharing state itself.
Part 1: What is a Custom Hook?
Before we build one, let's establish a clear mental model for when a custom hook is the right tool for the job.
Custom Hooks in React - Every React Developer Should Know This
The following short clip from the ByteGrad channel offers a perfect three-part rule for reusability in React.
Watch the first 30 seconds of this video. The speaker lays out a clear decision tree for what to create when you need to reuse logic.
As the video succinctly states:
- To reuse JSX/markup, create a Component.
- To reuse simple JavaScript logic (without React features), create a utility function.
- To reuse stateful logic involving React hooks, create a Custom Hook.
A custom hook is, at its core, a JavaScript function with two key properties:
- Its name must start with the word
use(e.g.,useFormInput). This is a convention that React's linter uses to enforce the Rules of Hooks. - It can call other hooks (e.g.,
useState,useEffect, or even other custom hooks).
This ability to call other hooks is what makes a custom hook "React-aware," distinguishing it from a plain utility function. It can tap into React's features like state, effects, and context.
Let's see how this works by extracting logic from a component.
Reusing Logic with Custom Hooks
The official React documentation provides an excellent example of identifying duplicated logic and extracting it into a custom hook. We'll look at the useOnlineStatus example.
Read the first part of this article, up to (but not including) the section 'Hook names always start with use'. Focus on how the logic from StatusBar and SaveButton is identified and moved into the useOnlineStatus function.
The key takeaway from this example is how the extraction changes the component code. Instead of focusing on the implementation (setting up state, adding and cleaning up event listeners), the component can now declaratively express its intent:
const isOnline = useOnlineStatus();
This makes the component cleaner, easier to read, and decouples it from the low-level browser APIs.
Part 2: Implementing a Custom Hook
Now that we understand the "why," let's build a simple custom hook from scratch. We'll start with a common UI pattern: toggling a value.
MASTER React Custom Hook Pattern with Real-World Examples || Day 06
The video 'MASTER React Custom Hook Pattern' by Tapas Adhikary provides a clear, step-by-step implementation of a useToggle hook.
Watch the segment from 00:15:04 to 00:26:10. Pay close attention to: The file naming convention (useToggle.js). The function signature, accepting an initialValue. The use of useState inside the hook to manage the toggle state. The structure of the returned value (an array containing the current value and the toggle function). How the hook is consumed in the ThemeSwitcher component using array destructuring.
Let's recap the structure of the useToggle hook:
- Define the function:
function useToggle(initialValue = false) { ... } - Use React primitives:
const [value, setValue] = useState(initialValue); - Encapsulate the logic:
const toggle = () => setValue(prev => !prev); - Return the public API:
return [value, toggle];
Returning Values: Array vs. Object
The useToggle hook returns an array, just like useState. This is a common pattern, but not the only one. You can also return an object.
Custom Hooks in React - Every React Developer Should Know This
Let's revisit the ByteGrad video to analyze the trade-offs between returning an array and an object from a custom hook.
Watch the segment from 00:15:52 to 00:19:07. The speaker demonstrates both approaches and discusses the pros and cons.
Here is a summary of the trade-offs:
-
Returning an Array (e.g.,
[value, toggle]):- Pro: Allows the consumer to name the destructured variables freely (e.g.,
const [isDark, toggleTheme] = useToggle();). This is great for avoiding naming collisions when using the same hook multiple times. - Con: The consumer must rely on the order of the elements. Getting the order wrong can lead to subtle bugs.
- Pro: Allows the consumer to name the destructured variables freely (e.g.,
-
Returning an Object (e.g.,
{ value, toggle }):- Pro: The order doesn't matter, and the names are explicit, which can make the code more self-documenting.
- Con: Renaming requires more verbose syntax (e.g.,
const { value: isDark, toggle: toggleTheme } = useToggle();).
The convention is to use an array when the hook is analogous to useState (returning a value and a setter). For hooks that return multiple, distinct values, an object is often clearer.
Part 3: Under the Hood - How Do Hooks Really Work?
Your interest in the underlying mechanics is key here. Hooks seem magical, but they are built on two simple concepts: JavaScript closures and arrays.
-
Closures: A closure gives a function access to its "lexical scope" (the variables that were in scope when it was created), even when the function is executed outside that original scope. When you call
useState, React creates a state variable and a setter function. That setter function "closes over" the internal state storage for your component, allowing it to modify it later. -
Arrays: On the initial render of your component, React creates an array to store the state for each hook call. On subsequent renders, React walks through this array. The first call to
useStategets the state fromhooks[0], the second call gets it fromhooks[1], and so on.
This array-based implementation is precisely why the Rules of Hooks exist:
- Rule 1: Only Call Hooks at the Top Level. If you call a hook inside a condition, loop, or nested function, the number of hook calls might change between renders. This would throw off the array indexing, causing React to retrieve the wrong state for a given hook.
- Rule 2: Only Call Hooks from React Functions. This ensures that hooks are only ever called within a component (or another hook) that has this internal array of state.
Deep dive: How do React hooks really work?
The article 'Deep dive: How do React hooks really work?' provides a brilliant explanation by building a simplified React clone from scratch. This will solidify your mental model.
Please read the sections 'Not Magic, just Arrays' and 'Deriving the Rules of Hooks'. Don't worry about implementing the code yourself. Focus on understanding the conceptual model: How currentHook acts as an index that increments with each hook call. How this simple array-and-index model immediately explains why the call order must be stable. Crucially, notice the quote: 'Custom Hooks simply fall out of the primitives provided by the framework.' This is the key insight: a custom hook is just a way to group several of these primitive hook calls together.
This "array-based" model demystifies hooks entirely. A custom hook isn't a special construct to React. When React renders your component, it just sees a flat list of primitive hook calls coming from both your component and any custom hooks you use.
Part 4: Sharing Logic, Not State
This is one of the most important concepts to grasp about custom hooks.
Let's say two different components both call useToggle(). Do they share the same true/false state? The answer is no.
Custom hooks share stateful logic, but not state itself.
Every call to a custom hook is completely independent and gets its own isolated state. When ComponentA calls useToggle(), it gets its own useState instance inside. When ComponentB calls useToggle(), it gets a completely separate useState instance.
Reusing Logic with Custom Hooks
The official React docs explain this critical distinction with a great useFormInput example.
Read the section 'Custom Hooks let you share stateful logic, not state itself'. The way it contrasts the two calls to useFormInput clearly illustrates how each call creates independent state.
Part 5: A More Advanced, Type-Safe Custom Hook
Let's build a more practical, real-world hook: useLocalStorage. This hook will synchronize a piece of React state with the browser's localStorage. This is a perfect example because it involves both useState (for the value) and useEffect (to handle the side effect of writing to localStorage).
Given your experience with TypeScript, we can also make this hook fully type-safe using generics.
Custom Hooks in React - Every React Developer Should Know This
This final video from ByteGrad walks through building a useLocalStorage hook, including making it generic with TypeScript. This aligns perfectly with building robust, reusable code.
Please watch the following segments: The Problem (00:06:15 - 00:11:40): See how the logic for getting and setting data to localStorage involves both useState and useEffect, making it a prime candidate for a custom hook. Extraction (00:11:40 - 00:15:03): Observe the process of extracting this logic into a useLocalStorage function, making it generic by accepting a key. Making it Type-Safe (00:20:10 - 00:26:17): This is the advanced part. Follow how a TypeScript generic <T> is used to create a type-safe relationship between the initialValue passed to the hook and the value it returns. This ensures that if you pass in a string, the hook knows the state is a string, and if you pass in an array, it knows it's an array.
The use of generics here (function useLocalStorage<T>(...)) is a powerful technique. It allows you to create a single hook that can work with any data type (string, number, object, etc.) while still providing full type safety and autocompletion in your editor, preventing a whole class of bugs.
Practice Challenge
Now it's your turn to apply these concepts. Let's adapt the useCounter challenge from the React documentation.
Your task:
Create a custom hook named useCounter.
- It should accept an
initialValueand astepvalue as arguments. - It should manage a count state, initialized with
initialValue. - It should return an object containing:
- The current
count. - An
incrementfunction. - A
decrementfunction. - A
resetfunction.
- The current
- Use this hook in a component to display the count and buttons to control it.
Think about the structure: you'll need useState to hold the count and you'll return an object with the value and the manipulator functions.
Conclusion
In this lesson, we demystified custom hooks, revealing them as a logical extension of React's core principles rather than a magical feature.
Key Takeaways:
- Purpose: Custom hooks allow you to extract and reuse stateful logic (logic that uses other hooks like
useStateoruseEffect) from your components. - Convention: They are JavaScript functions whose names must start with
use. - Mechanism: They work because of JavaScript closures (to "remember" state) and React's internal array-based state management, which requires a stable hook call order (the Rules of Hooks).
- Core Principle: Custom hooks share stateful logic, but each call to a hook creates its own independent state.
- Implementation: You can design their API to return values in an array (for flexibility) or an object (for clarity).
What's Next?
We've now seen how to encapsulate and reuse logic. But what about sharing the state itself across many components that are far apart in the component tree? Passing props down through many layers, known as "prop drilling," can become cumbersome.
In our next lesson, we will tackle this exact problem by exploring the Context API. We will see how it provides a way to pass data through the component tree without having to pass props down manually at every level, and we'll compare when to use Context versus lifting state up.