The Context API is quite trendy these days and people say that it might even be good enough to replace redux. In the following lines, we will give it a try.
In this article we will setup a minimal frontend project, using the Context API to make some requests and update the content of some components, of course using Typescript and hooks. Related to the hooks, we will make use of useContext
and useReducer
, so that we will dispatch actions which will modify the globally stored state.
Important note: the code inside this article should be treated as sample and easy access / general overview. The complete code is available on Github:
Initial setup
You need some version of node installed on your computer. My current version is v14.15.3
. We will use npx
and yarn
with it. Just to keep things simple, we will use create-react-app
as a starting point:
npx create-react-app pokemons-with-context-api --template typescript
We will try to keep the project as small and simple as possible. Thus, we will make some decisions upfront and set some general rules to apply within the codebase:
- Components, folders, variables will be named using
camelCase
andPascaleCase
; - For keeping the project small, we will use
fetch
for making the api requests; - We will have a global state, defined using the Context API.
Setting up the global context
We will store the contexts inside a new folder which we will create in the root of the project, namely ./contexts
. Therefore, our global context will have the following structure:
./contexts/globalContext/index.ts
– exports the components and hooks created;./contexts/globalContext/hooks/useGlobalContext.ts
– contains a hook which will basically provide easy access to the context data;./contexts/globalContext/GlobalContext.tsx
– the component for the context itself;./contexts/globalContext/GlobalProvider.tsx
– the component for the provider;./contexts/globalContext
/types.ts
– defining the types outside the two components can make the code easier to understand.
Once the context is properly defined, we need to wrap the application component inside the GlobalProvider
, and we will do that in the root file of the whole application, namely the index.tsx
file.
Sources:
GlobalContext.tsx
This component will contain the type and the initial value of the context. We will also import from the provider the type of the context, mainly because the type of the dispatch function is defined in there.
import { createContext } from 'react'
import { GlobalContextType } from './GlobalProvider'
export type GlobalStateType = {
pokemonName: string
pokemonAvatar: string
error: string
isLoading: boolean
}
/**
* The initial state
*/
export const initialState: GlobalStateType = {
pokemonName: '',
pokemonAvatar: '',
error: '',
isLoading: false,
}
/**
* The context
*/
export const GlobalContext = createContext<GlobalContextType>({
state: initialState,
dispatch: () => undefined,
})
export default GlobalContext
GlobalProvider.tsx
import { useReducer, Dispatch, ReactNode } from 'react'
import GlobalContext, { initialState, GlobalStateType } from './GlobalContext'
export type GlobalActionType =
| {
type: 'setPokemonName' | 'setPokemonAvatar' | 'setError'
payload: string
}
| {
type: 'setIsLoading'
payload: boolean
}
export type GlobalContextType = {
state: GlobalStateType
dispatch: Dispatch<GlobalActionType>
}
const globalContextReducer = (state: GlobalStateType, action: GlobalActionType): GlobalStateType => {
switch (action.type) {
case 'setPokemonName': {
console.log(`Setting the name of the pokemon to: ${action.payload}`)
return {
...state,
pokemonName: action.payload,
pokemonAvatar: '',
error: '',
}
}
case 'setPokemonAvatar': {
console.log(`Updating the avatar for: ${state.pokemonName}`)
return {
...state,
pokemonAvatar: action.payload,
error: '',
}
}
case 'setError': {
console.log(`Error: ${action.payload}`)
return {
...state,
error: action.payload,
}
}
case 'setIsLoading': {
console.log(`Set isLoading: ${action.payload}`)
return {
...state,
isLoading: action.payload,
}
}
default:
return state
}
}
export const GlobalProvider = (props: { children: ReactNode }): JSX.Element => {
const [state, dispatch] = useReducer(globalContextReducer, initialState)
return (
<GlobalContext.Provider
value={{
state,
dispatch,
}}
>
{props.children}
</GlobalContext.Provider>
)
}
export default GlobalProvider
useGlobalContext.ts
This hook is mainly created to provide easy access to the context inside the components which need it.
import { useContext } from 'react'
import GlobalContext from '../GlobalContext'
import { GlobalContextType } from '../GlobalProvider'
const useGlobalContext = (): GlobalContextType => useContext(GlobalContext)
export default useGlobalContext
Create the pokemons request
We will store the request inside a custom hook. It will contain minimal protection to ensure that only one request happens at a time. For the sake of simplicity, we will use fetch
.
import { useEffect } from 'react'
import { useGlobalContext } from '../contexts'
const useFetchPokemon = () => {
const {
state: { pokemonName },
dispatch,
} = useGlobalContext()
useEffect(() => {
let isCurrent = true
if (pokemonName.length < 3) {
return
}
fetch(`https://pokeapi.co/api/v2/pokemon/${pokemonName}/`)
.then((res) => res.json())
.then((res) => {
if (isCurrent && !!res.sprites && !!res.sprites.front_default) {
dispatch({
type: 'setPokemonAvatar',
payload: res.sprites.front_default,
})
}
})
.catch((e) => {
dispatch({ type: 'setError', payload: e.message })
})
return () => {
isCurrent = false
}
}, [pokemonName, dispatch])
}
export default useFetchPokemon
A more elegant solution is to use the AbortController
:

Sources:
Create components which use the context API
The next step is to create an input which will set the name of the pokemon we are looking for. Just for the fun of it, as it is not necessary for this project, we will add some debouncing.
At this point, it is important to remember that we are building a small demo which uses the Context API – this means that we are separating components more than it might seem necessary. We will mainly implement two components which access the global context:
./components/Search.tsx
– shows the input for updating the pokemon’s name;./components/Avatar.tsx
– displays the avatar of the pokemon, if available.
Sources:
- https://blog.bitsrc.io/improve-your-react-app-performance-by-using-throttling-and-debouncing-101afbe9055
- https://gist.github.com/ca0v/73a31f57b397606c9813472f7493a940
- https://redd.one/blog/debounce-vs-throttle
Search.tsx
import { useState, useRef, useEffect } from 'react'
import { useGlobalContext } from '../contexts'
import debounce from '../utils/debounce'
import { useFetchPokemon } from '../hooks'
const Search = (): JSX.Element => {
const {
state: { pokemonName, isLoading, error },
dispatch,
} = useGlobalContext()
const [searchInput, setSearchInput] = useState(pokemonName)
useFetchPokemon()
console.log({ pokemonName, searchInput })
const debounced = useRef(
debounce((value: string) => {
dispatch({
type: 'setPokemonName',
payload: value,
})
}, 500)
)
useEffect(() => debounced.current(searchInput), [searchInput])
return (
<>
<input
className="Search"
type="text"
value={searchInput}
onChange={(e) => {
setSearchInput(e.target.value)
dispatch({
type: 'setPokemonName',
payload: '',
})
}}
/>
{isLoading && <div className="Loading">Loading...</div>}
{!!error && <div className="Error">An error occurred while retrieving the data.</div>}
</>
)
}
export default Search
Avatar.tsx
import { useGlobalContext } from '../contexts'
const Avatar = (): JSX.Element => {
const { state } = useGlobalContext()
return <>{state.pokemonAvatar && <img src={state.pokemonAvatar} alt={state.pokemonName} />}</>
}
export default Avatar
Improve the test coverage
We have very little tests at this point. We will use jest and its __tests__
/ *.spec.ts
folder structure. We will not aim to having proper coverage. So the purpose just show some examples of how to test components. Using blackbox testing is preferred.
Sources:
- https://feralamillo.medium.com/create-react-app-typescript-testing-with-jest-and-enzyme-869fdba1bd3
- https://www.guru99.com/black-box-testing.html
- https://www.leighhalliday.com/mock-fetch-jest
Conclusion
Having the possibility to create small projects, without setting up the whole redux boilerplate is quite useful.
However, the Context API is not meant to replace redux. Or if it is, it’s just not there yet. The main drawback is that everything which consumes a context, will be re-rendered when the values inside the context are updated.