Context and Custom Hooks
Context
React context allows us to pass down and consume data in any component without using props. Makes it easy to share data across components.
It is the equivalent of global variables for our React components.
It avoid prop drilling.
When to use?
- Theme data (light or dark)
- User data (currently authenticated user)
- Location-specific data (user language or locale)
- For data that does not need to be updated often
How to use?
- Create context using
createContext
method - Wrap the context provider around the component tree
- Put any value on your context provider with the
value
prop - Read the value using
useContext
Code Example
import { useEffect, useState, createContext, useContext } from 'react';
interface Pokemon {
"id": number,
"name": string,
"type": string[],
"hp": number,
"attack": number,
"defense": number,
"special_attack": number,
"special_defense": number,
"speed": number
}
const PokemonContext = createContext<ReturnType<typeof usePokemonSource>>(
{} as unknown as ReturnType<typeof usePokemonSource>
);
function usePokemonSource() {
const [pokemon, setPokemon] = useState<Pokemon[]>([]);
useEffect(() => {
fetch("/pokemon.json")
.then((response) => response.json())
.then((data) => setPokemon(data))
}, [])
return {pokemon};
}
export function usePokemon() {
return useContext(PokemonContext);
}
export function PokemonProvider({
children,
}: {
children: React.ReactNode;
}) {
return (
<PokemonContext.Provider value={usePokemonSource()}>
{children}
</PokemonContext.Provider>
);
}
Custom Hooks
When you have component logic that needs to be used by multiple components, we can extract that logic to a custom hook.
Simply, extract the data fetching logic to a different file and export the function that returns the data and use it in your components.
Code Example
In this example, the function usePokemon
can be termed as a custom hook.
import { useEffect, useState, createContext, useContext, useReducer, useCallback, useMemo } from 'react';
interface Pokemon {
"id": number,
"name": string,
"type": string[],
"hp": number,
"attack": number,
"defense": number,
"special_attack": number,
"special_defense": number,
"speed": number
}
const PokemonContext = createContext<ReturnType<typeof usePokemonSource>>(
{} as unknown as ReturnType<typeof usePokemonSource>
);
function usePokemonSource(): {
pokemon: Pokemon[];
search: string;
setSearch: (search: string) => void;
} {
//const [pokemon, setPokemon] = useState<Pokemon[]>([]);
//const [search, setSearch] = useState("");
type PokemonState = {
pokemon: Pokemon[],
search: string;
}
type PokemonAction = { type: "setPokemon"; payload: Pokemon[] } |
{ type: "setSearch"; payload: string };
const [ {pokemon, search}, dispatch ] = useReducer((state: PokemonState, action: PokemonAction) => {
switch(action.type) {
case "setPokemon":
return { ...state, pokemon: action.payload };
case "setSearch":
return { ...state, search: action.payload };
}
}, {
pokemon: [],
search: ""
})
useEffect(() => {
fetch("/pokemon.json")
.then((response) => response.json())
.then((data) => dispatch({
type: "setPokemon",
payload: data
}))
}, []);
const setSearch = useCallback((search: string) => {
dispatch({
type: "setSearch",
payload: search,
});
}, []);
const filteredPokemon = useMemo(
() => pokemon.filter((p) => p.name.toLowerCase().includes(search)).slice(0, 20),
[pokemon, search]
)
const sortedPokemon = useMemo(() =>
[...filteredPokemon].sort((a, b) => a.name.localeCompare(b.name)), [filteredPokemon]);
return { pokemon: sortedPokemon, search, setSearch };
}
export function usePokemon() {
return useContext(PokemonContext);
}
export function PokemonProvider({
children,
}: {
children: React.ReactNode;
}) {
return (
<PokemonContext.Provider value={usePokemonSource()}>
{children}
</PokemonContext.Provider>
);
}