Advanced Hooks & Component Patterns
Master advanced React hooks like useContext, useReducer, and learn how to create custom hooks. Explore powerful component patterns for building scalable applications.
In this lesson, we'll build a Toast notification component from scratch using React's advanced hooks. You'll learn how to manage global state with useReducer
and useContext
, and how to encapsulate logic in a custom hook. By the end, you'll know how to avoid prop-drilling with context and handle complex state updates with a reducer.
In this lesson, we will cover:
- Setting up a reducer to manage notifications state (adding/removing toasts)
- Providing global access to notifications via React Context
- Creating a custom hook to use the notification context in any component
- Building the Toast UI and using the hook to trigger notifications
Step-by-step: build it right
Managing notifications state with useReducer
In a real app, you might have multiple toast notifications at once. We need a state structure (like an array of messages) and functions to add or remove notifications. Using useReducer is ideal for this, because it lets us define state transitions in one place. In fact, useReducer is an alternative to useState that helps manage complex state logic in React apps. We'll use it to handle our list of toasts with clear actions for adding and removing items.
First, define the shape of a toast and set up a reducer function. In TypeScript, we can declare an interface for a Toast object and define action types for adding or removing a toast. The reducer will take the current state (list of toasts) and an action, then return the new state:
Loading syntax highlighting...
We initialize with an empty list of toasts. The 'ADD_TOAST' case returns a new array including the new toast (we spread the old state to avoid mutating it). The 'REMOVE_TOAST' case filters out the toast with the given id. Always return a new array instead of mutating state in place – this ensures React knows the state changed. Generating a unique id (like using Date.now()) for each toast is important so we can identify and remove it later.
Providing global notifications with useContext
Next, we want to make this notification state accessible from anywhere in the app. We'll use React Context to avoid passing props down through many layers. The Context API lets us share state without prop drilling. We create a context for our toasts and a provider component to wrap our app.
First, create a context with the proper TypeScript type for the value it will provide. This value will include the list of toasts and functions to add or remove a toast:
Loading syntax highlighting...
We use createContext
to make a ToastContext. The context holds a ToastContextType object with our state and operations. We'll set it to undefined initially – that helps us catch errors if a component tries to use the context without a provider. (In TypeScript, you could also use null or a dummy default value, but undefined and a runtime check is a common pattern for required context.)
Now we implement the Provider component. This component will use the toastReducer we wrote and pass the state and actions into the context. It will also render the UI for the toast notifications:
Loading syntax highlighting...
Let's break down what's happening here. We use useReducer to get the current toasts array and a dispatch function. We then define two helper functions:
- addToast creates a new toast object (assigning a unique id), and dispatches an action to add it. We allow an optional type (like "success" or "error") with a default of "info" for styling or icons if needed.
- removeToast dispatches an action to remove a toast by its id.
We include these functions and the current toasts in the context value. Now any child component wrapped by ToastProvider can access the list of toasts and call addToast or removeToast without drilling props. The JSX at the end of the provider renders the children (the rest of your app) and also a container for the toast messages. We map over the toasts array and render each message in a basic div. Each toast has a close "×" button that calls removeToast(t.id) to dismiss it. We're keeping the UI minimal: just text and a button. In a real library component, you might add icons, animations, or styling, but our focus here is the functionality.
Note: We gave each toast element a
key={t.id}
attribute. Using a unique key for list items is important so React can efficiently update and remove them. If you forget keys, React will warn you and you may see odd behavior when removing items.
Also notice that the context value is an object containing functions. React will re-create these functions on each render of the provider, which means consumers could re-render often. For our small use case this is fine. In larger apps, you might wrap addToast and removeToast in useCallback or use useMemo to optimize, but we won't complicate things here.
After defining ToastProvider, don't forget to wrap your application with it. For example, in your entry file (e.g. index.tsx or App.tsx), wrap the app component:
Loading syntax highlighting...
This ensures the context is available to all components inside <App />
. If you skip this, any useContext(ToastContext)
call will get undefined – always wrap the tree with the provider!
Creating a custom hook for the toast context
Our context is ready, but using it involves calling useContext(ToastContext)
in every component that needs to show a toast. We can simplify this with a custom hook. A custom hook is just a function that uses other hooks (like useContext) to encapsulate logic you want to reuse. In React, "Custom hooks are your way of creating reusable logic that can be shared among multiple components." We'll create a useToast
hook that returns our context value (the toasts, addToast, removeToast):
Loading syntax highlighting...
This hook simply calls useContext(ToastContext)
for us. We include a safety check: if there's no context value (meaning the component isn't wrapped in ToastProvider), we throw an error. This pattern helps catch misuse early. Now anywhere in our app, instead of writing useContext(ToastContext)
, we can import and use useToast()
to get the same object. It's a bit cleaner and abstracts away the context details. The hook name starts with "use" to follow React's convention — this is important so that React knows it's a hook and can apply the Rules of Hooks.
Using the toast system in your app
Now for the fun part: using our new Toast notification system in a component. Because we set up context, any component under ToastProvider can trigger a toast. For example, imagine Ollie the Owl has a Save button that should show a confirmation message when clicked. We can use useToast
inside that component:
Loading syntax highlighting...
When the user clicks "Save settings", handleSave
runs and calls addToast
. We pass a message and a type 'success' (maybe our CSS will give success toasts a green border, for example). Because addToast
updates state via our reducer, the ToastProvider will re-render its list of toasts, and the new message will appear on screen. This could be a small pop-up box in the corner – exactly what a toast is. If the user clicks the "×" on the toast, it will call removeToast
and the message will disappear.
You can use addToast
from any component where you call useToast()
. This could be in response to form submissions, server responses, or any event where you need to notify the user. And because we designed the Toast component to be part of a component library, it's reusable across the app. We could even extend it with more features (like auto-dismiss after a timeout, different styles for different type, etc.), but the core pattern would remain the same.
Best practices & common mistakes – avoid dumb bugs
When using these advanced hooks and patterns, keep in mind a few common pitfalls:
- Always wrap with the Provider: If you forget to wrap your app (or a section of it) with
<ToastProvider>
, calls touseToast
will return undefined. This will either cause an error or, if unchecked, nothing will happen. Ensure your provider is high up in the tree so all components that need it are within its context. - Do not mutate state in the reducer: Inside the reducer, never modify the existing state array (e.g. don't do
state.push(...)
). Always return a new array or object. For example, we used spread[...]
andfilter
to produce new arrays. Mutating state will prevent React from detecting changes and can lead to bugs where the UI doesn't update. - Use unique keys for list items: When rendering a list of components (toasts in our case), give each item a unique key (we used the toast's id). Forgetting the key can cause rendering issues and React warnings. It's a small detail that saves a lot of headache in list rendering.
- Mind the context value reference: Our ToastContext.Provider value is an object that changes on every render (because it contains new functions). This is okay for small apps, but be aware that context triggers re-renders of consumers whenever the value reference changes. To avoid unnecessary re-renders, you could optimize by memoizing the context value. In our simple example we didn't need this optimization, but it's good to remember for larger contexts.
- Name custom hooks with "use": If you create a custom hook, always start its name with use. This is not only a convention but also required for React's hook checks. A custom hook that doesn't start with "use" might not work correctly, and linters will warn about it. For instance,
useToast
is correct, whereas naming ittoastHelper
would be a mistake.
⚠️ Don't call hooks inside conditional logic or loops.
By following these best practices, you'll avoid the common bugs that can crop up when using context and reducers in React.
Recap
- We built a Toast notification component using advanced React hooks and patterns. This involved managing a list of notifications globally, rather than in a single component.
- You learned how to use useReducer for state that has complex update logic or multiple sub-values. In our case, the reducer cleanly handled adding and removing toast entries without us manually handling state merges.
- We utilized React Context (useContext) to provide that state and its updater functions to any component in the app. This eliminated the need to pass props down through many layers, solving the prop drilling problem.
- We created a custom hook
useToast
to simplify accessing the toast context. Custom hooks let you encapsulate reusable logic (in this case, consuming context) so that components can use a simple API to interact with the notification system. - With these tools, Ollie the Owl (and you!) can now add a global notification/toast system to a React app as part of a growing component library. You can trigger toast messages from anywhere, making your app more user-friendly with timely feedback. And you've seen a repeatable pattern – using context + reducer + custom hook – that can be applied to many other scenarios requiring global state management without heavy libraries.
Now you have a solid grasp of advanced hooks and component patterns. You can confidently manage global state and side-step common React headaches while building reusable components. Happy coding!