Managing Data in React

Matthew MacFarquhar
8 min readJul 17, 2024

--

Introduction

I have been reading this book on React 18 and its best practices. As I go through this book, I will distill the core pieces of the most interesting tid-bits in this series of articles on React. In this article we will talk about how to manage the global state of an application using Redux and the useContext hook. We will also discuss utilizing react suspense to show fallback components whilst our components load.

UseContext

The useContext hook is a great way to manage and share global data throughout our app and is often used as an alternative to Redux.

In this example, we will create a context to fetch github issues from a provided url.

import axios from "axios"
import { FC, ReactNode, createContext, useCallback, useEffect, useState } from "react"

export type Issue = {
number: number,
url: string,
title: string,
state: string
}

interface Props {
url: string,
children: ReactNode
}

export const IssueContext = createContext({issues: [], url: ''});

export const IssueProvider: FC<Props> = ({children, url}) => {
const [issues, setIssues] = useState([]);
const fetchIssues = useCallback(async () => {
const response = await axios(url);
if (response) {
setIssues(response.data)
}
}, [url]);

useEffect(() => {
fetchIssues();
}, [fetchIssues]);

const context = {issues, url}

return <IssueContext.Provider value={context}>{children}</IssueContext.Provider>
}

We begin by creating a context using the createContext function which will take in the fields and default values we want to provide to consumers. In our case, these will be issues and the url.

To use our context, we will need to wrap components in a provider. We create the Higher Order Component IssueProvider which takes in its children and the url to pull the github issues from.

Our IssueProvider has some state for the issues and utilizes useEffect to pull in the data from github whenever the url changes.

It finally returns the children components wrapped with the IssueContext Provider and our loaded context values.

When we want to use the context, we must wrap the components that want it inside the Provider HOC like so.

import { Issues } from "./Issues"
import { IssueProvider } from "./contexts/Issue"

export const App = () => {
return (
<IssueProvider url="https://api.github.com/repos/ContentPI/ContentPI/issues">
<Issues/>
</IssueProvider>
)
}

Then we can call useContext and grab the shared data from anywhere below the Provider in the ReactNode hierarchy.

import { FC, useContext } from "react"
import { Issue, IssueContext } from "./contexts/Issue"

export const Issues: FC = () => {
const { issues, url } = useContext(IssueContext);
return (
<>
<h1>ContentPI Issues From Context</h1>
{issues.map((issue: Issue) => (
<p key={`issue-${issue.number}`}>
<strong>#{issue.number}</strong> {' '}
<a href={`${url}/${issue.number}`}>{issue.title}</a> {' '}
{issue.state}
</p>
))}
</>
)
}

Using this method, we can make contexts which are globally available as well as contexts which are only available in specific child components all with basic React functionality.

React Suspense

Often times, it will take time to load in components or data to populate our pages, it is often quite a bad UX to show a blank page while nothing is ready yet. In this section we will use React Suspense and SWR to add loading skeletons when data is being fetched for a better UX.

LoadingSkeleton

You have probably seen a loading skeleton in most apps you use on your phone or online, they usually are grey-ish bars with a little animation. react-loading-skeleton provides us with some styles and components so we don’t have to build these skeletons from scratch.

import { FC } from "react";
import Skeleton from "react-loading-skeleton";

export const LoadingSkeleton: FC = () => (
<div>
<Skeleton height={200} width={200}/>
</div>
);

It is important to include the css somewhere either in your main.tsx or App.tsx or else the skeletons will not be visible.

import 'react-loading-skeleton/dist/skeleton.css'

Adding Suspense

To add suspense we first need a data fetcher

export const fetcher = (url: string) => {
return fetch(url).then((response) => {
if (response.ok) {
return response.json()
} else {
return {error: true}
}
})
}

This fetcher function will return either the response json or simple json with error = true.

In our top level component we can config SWR to use our fetcher and turn on suspense.

import { SWRConfig } from "swr"
import { StyledPokedex, StyledTitle } from "./components/Pokemon/Pokemon.styled"
import { fetcher } from "./components/Pokemon/fetcher"
import { PokeContainer } from "./components/PokeContainer"

export const App = () => {
return (
<>
<StyledTitle>Pokedex</StyledTitle>
<SWRConfig value={{fetcher: fetcher, suspense: true}}>
<StyledPokedex>
<PokeContainer/>
</StyledPokedex>
</SWRConfig>
</>
)
}

Let’s drill through the child components now

import { Suspense } from "react"
import { Pokedex } from "./Pokemon/Pokedex"

export const PokeContainer = () => {
return (
<Suspense fallback={<h2>Loading Pokedex...</h2>}>
<Pokedex/>
</Suspense>
)
}

The PokeContainer will show a fallback <h2/> while the Pokedex component loads its data.

import { Suspense } from "react";
import useSWR from "swr"
import { StyledGrid } from "./Pokemon.styled";
import { LoadingSkeleton } from "./LoadingSkeleton";
import { Pokemon } from "./Pokemon";

export const Pokedex = () => {
const { data: {results} } = useSWR('https://pokeapi.co/api/v2/pokemon?limit=150');

return (
<>
{results.map((pokemon: {name: string}) => (
<Suspense fallback={<StyledGrid><LoadingSkeleton/></StyledGrid>} key={pokemon.name}>
<Pokemon pokemonName={pokemon.name}/>
</Suspense>
))}
</>
)

The Pokedex component will use our fetcher to call the pokeapi. Once it has the data, it will render the 150 Pokemon components and show a 200x200 loading skeleton as a fallback for the ones that have not loaded yet.

import { FC } from "react"
import useSWR from "swr"
import { StyledCard, StyledHeader, StyledType, StyledTypes } from "./Pokemon.styled"

type Props = {
pokemonName: string
}

export const Pokemon: FC<Props> = ({pokemonName}) => {
const { data, error } = useSWR(`https://pokeapi.co/api/v2/pokemon/${pokemonName}`);

if (error || data.error) {
return <div/>
}

const {id, name, sprites, types} = data;
const pokemonTypes = types.map((pokemonType: any) => pokemonType.type.name);

return (
<StyledCard pokemonType={pokemonTypes[0]}>
<StyledHeader>
<h2>{name}</h2>
<div>#{id}</div>
</StyledHeader>
<img alt={name} src={sprites.front_default}/>
<StyledTypes>
{pokemonTypes.map((pokemonType: string) => (
<StyledType key={pokemonType}>{pokemonType}</StyledType>
))}
</StyledTypes>
</StyledCard>
)
}

Each Pokemon component will call the pokeapi to get in depth information about its pokemon which will be used to populate the fields of the component.

You can turn on throttling in devtools to slow down your network and see the Suspense fallbacks we have provided whilst the Pokedex and Pokemon components load their data from the pokeapi.

Redux

React Redux is a toolset for using Redux in your react applications. Since we have useContext now, I consider Redux to be the legacy way of managing global state but it is definitely worth learning as there are sure to be many projects that still actively use it.

Redux consist of a store which allows us to onboard many slices for different contexts in our app. These contexts and their values and/or functions can be accessed via the store.

Creating the Store

To create the store we first need to build our reducers. A slice (i.e. slice of the entire reducer) defines some state with default values and some reducers which can preform some update on the state.

The reducers are exported as actions and the entire slice is exported as the default.

import { createSlice } from "@reduxjs/toolkit";

const counterSlice = createSlice({
name: 'counter',
initialState: 0,
reducers: {
increment: (state) => state + 1,
decrement: (state) => state -1
}
});

export const {increment, decrement} = counterSlice.actions;
export default counterSlice.reducer;

We can combine our reducers to create a unified rootReducer.

import { combineReducers } from "@reduxjs/toolkit";
import counterReducer from "./counterSlice";

const rootReducer = combineReducers({
counter: counterReducer
});

export default rootReducer;

Which we can then use to configure our store.

import { configureStore } from "@reduxjs/toolkit";
import rootReducer from "./rootReducer";

const store = configureStore({
reducer: rootReducer
});

export type RootState = ReturnType<typeof rootReducer>;
export default store;

Once we have our store, we can wrap our App inside a Provider Higher Order Component with our synthesized store.

import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux'
import store from './store.ts'
import { Counter } from './Counter.tsx'

ReactDOM.createRoot(document.getElementById('root')!).render(
<Provider store={store}>
<Counter/>
</Provider>
)

Using the Store

To use the store inside a child component, we will use useSelector (to grab a state value) and useDispatch (to call some reducer function).

import { useDispatch } from "react-redux";
import { RootState } from "./store"
import { useSelector } from "react-redux"
import { decrement, increment } from "./counterSlice";

export const Counter = () => {
const count = useSelector((state: RootState) => state.counter);
const dispatch = useDispatch();

return (
<div>
<button onClick={() => dispatch(decrement())}>-</button>
<span>{count}</span>
<button onClick={() => dispatch(increment())}>+</button>
</div>
)
}

Here we grab the count state so we can render it to the screen, we have increment and decrement buttons which dispatch out increment and decrement functions defined in our Counter slice.

Conclusion

In this article, we learned how to manage and share global state in our apps using Redux or the useContext hook. We also learned how to provide a better UX by using Suspense and loading skeletons to show loading indicators whilst components load the data necessary for them to render properly.

Appendix

Below are some files you will need if you want to re-produce the examples given.

Pokemon.styled.ts

import styled from "styled-components"

const typeToColor: any = {
bug: '#2ADAB1',
dark: '#636363',
dragon: '#E9B057',
electric: '#ffeb5b',
fairy: '#ffdbdb',
fighting: '#90a4b5',
fire: '#F7786B',
flying: '#E8DCB3',
ghost: '#755097',
grass: '#2ADAB1',
ground: '#dbd3a2',
ice: '#C8DDEA',
normal: '#ccc',
poison: '#cc89ff',
psychic: '#705548',
rock: '#b7b7b7',
steel: '#999',
water: '#58ABF6'
}

export const StyledPokedex = styled.div`
display: flex;
flex-wrap: wrap;
flex-flow: row wrap;
margin: 0 auto;
width: 90%;
&::after {
content: '';
flex: auto;
}
`
type Props = {
pokemonType: string
}

export const StyledCard = styled.div<Props>`
position: relative;
${
({pokemonType}) =>
`
background: ${typeToColor[pokemonType]} url(./pokeball.png) no-repeat;
background-size: 65%;
background-position: center;
`
}
color: #000;
font-size: 13px;
border-radius: 20px;
margin: 5px;
width: 200px;

img {
margin-left: auto;
margin-right: auto;
display: block;
}
`

export const StyledTypes = styled.div`
display: flex;
margin-left: 6px;
margin-bottom: 8px;
`

export const StyledType = styled.span`
display: inline-block;
background-color: black;
border-radius: 20px;
font-weight: bold;
padding: 6px;
color: white;
margin-right: 3px;
opacity: 0.4;
text-transform: capitalize;
`

export const StyledHeader = styled.div`
display: flex;
justify-content: space-between;
width: 90%;

h2 {
margin-left: 10px;
margin-top: 5px;
color: white;
text-transform: capitalize;
}

div {
color: white;
font-size: 20px;
font-weight: bold;
margin-top: 5px;
}
`

export const StyledTitle = styled.h1`
text-align: center;
`

export const StyledGrid = styled.div`
flex-wrap: wrap;
flex-flow: row wrap;
div {
margin-right: 5px;
margin-bottom: 5px;
}
`

--

--

Matthew MacFarquhar
Matthew MacFarquhar

Written by Matthew MacFarquhar

I am a software engineer working for Amazon living in SF/NYC.

No responses yet