React Custom Context Hook
Updatable React context Hook with custom logic in TypeScript.
React Hooks brings status management to functions and enables us to get rid of JavaScript classes whatsoever. Now, both components and Hooks are mere functions.
Hooks are a good idea. So good that it was worth mentioning in the Technology Radar.
As React is an unopinionated framework, it is hard to find a consistent set of good practices and integrous architectural principles.
The same is true for Hooks. Hooks can be used in different ways, based on the use-case or personal taste.
The Context Hook is one of the less common but still very useful built-in Hooks.
Context Hook
Using context makes it easy to share data through components without passing it as properties.
Practically, it addresses awkward situations like this:
const App = () => { const theme = "dark"; return ( <Menu theme={theme} /> <Content theme={theme} /> <Footer theme={theme} /> ); } const Menu = ({theme}}) => { return ( <div> <Button theme={theme} label="Home" /> <Button theme={theme} label="About" /> </div> ); } const Button = ({theme, label}) => { return ( <a className={theme}> {label} </a> ) } ...
The build-in Hook useContext
accepts a context object and returns the current context value:
const value = useContext(MyContext);
A component calling useContext
will always re-render when the context value changes.
The solution for the problem above looks like follows:
const ThemeContext = createContext("light"); const App = () => { return ( <ThemeContext.Provider value="dark"> <Menu /> <Content /> <Footer /> </ThemeContext.Provider> ); } const Menu = () => { return ( <div> <Button label="Home" /> <Button label="About" /> </div> ); } const Button = ({label}) => { const theme = useContext(ThemeContext); return ( <a className={theme}> {label} </a> ) } ...
To provide functionality for changing the current value of the context, we must use it together with other Hooks. Combining multiple Hooks to work out a common goal is a case for a custom Hook.
Custom Context Hook
A custom Hook is a function whose name starts with use
and that may call other Hooks.
Our Hook will create a context for holding a value of the current theme (light or dark). The user can change the theme anytime. The current value will be stored in localStorage
to be used on the next visit.
We will create our useTheme
TypeScript-first:
// useTheme.tsx import {createContext, useContext, useState} from "react"; // context object structure type ContextType = { theme: string; updateTheme: (theme: string) => void; }; // create an empty context const ThemeContext = createContext<ContextType>({ theme: "", updateTheme: () => {} }); // context provider container export const ThemeProvider = (prop: {value?: string, children: JSX.Element | JSX.Element[]}) => { const [theme, setTheme] = useState<string>( localStorage.getItem("theme") || prop.value || "light"); const updateTheme = (theme: string) => { setTheme(theme); localStorage.setItem("theme", theme); }; return ( <ThemeContext.Provider value={{theme, updateTheme}}> {prop.children} </ThemeContext.Provider> ); }; // custom context hook const useTheme = () => useContext(ThemeContext) as ContextType; export default useTheme;
Our context object holds the current value and update function. To take the complicated initialization away from the user code, we provide a convenient ThemeProvider
function. This provider container initializes the context with a state and the update function. The state is initialized with the value from localStorage
or a user-provided default value respectively. The update function actualizes the state and storage when the context value has changed.
Next, we will use our Hook in the components. The whole component tree must be closed in the context provider:
// App.tsx import Menu from "./Menu"; import Content from "./Content"; import Footer from "./Footer"; import {ThemeProvider} from "./useTheme"; const App = () => { return ( <ThemeProvider value="dark"> <Menu /> <Content /> <Footer /> </ThemeProvider> ); } export default App;
Now, all children of the provider have access to its context.
We will use the update function to change the theme based on the user’s preferences:
// Menu.tsx import Button from "./Button"; import useTheme from "./useTheme"; const Menu = () => { // update function from the context const {updateTheme} = useTheme(); const lighten = () => updateTheme("light"); const darken = () => updateTheme("dark"); return ( <div> <nav> <a onClick={lighten}>🌝</a> <a onClick={darken}>🌚</a> </nav> <Button label="Home" /> <Button label="About" /> </div> ); } export default Menu;
Finally, the value is used to render components with the corresponding theme:
// Button.tsx import useTheme from "./useTheme"; import "./Button.css"; const Button = (prop: {label: string}) => { // receive the current context value const {theme} = useTheme(); return ( <button className={theme}> {prop.label} </button> ) } export default Button;
Source Code
A demo application with mentioned code is on my GitHub.
Happy reacting!