Enhancing State Management with React Context and TypeScript

Enhancing State Management with React Context and TypeScript

Aug 27 2023

Contexts play a pivotal role in React development. They enable efficient management of shared state and data across components, facilitating seamless communication between different parts of your application. Understanding how to harness the power of contexts is paramount for building robust and maintainable React applications.


What is Contexts ?

Contexts in React are a mechanism for efficiently passing data through the component tree without manually passing props at each level. Think of them like a global chest for your application's data.

To illustrate this with an everyday example, imagine you're planning a family trip. Your family members represent different components of your application. Instead of calling each family member individually to provide them with the trip details, you can use a whiteboard (context) in your living room where everyone can see the plans. Now, if you update the departure time on the whiteboard, everyone immediately knows about the change.

In React, this is similar to updating data in a context, and any component that subscribes to that context will automatically receive the updated information without the need for direct communication between the components. It streamlines data sharing and ensures consistency throughout your application, much like that handy whiteboard for your family trip planning.


Advantages

Contexts offer a multitude of advantages in React development, making them an indispensable tool for building scalable and maintainable applications. Some key benefits include:

  • Simplified Data Sharing: Contexts eliminate the need for prop drilling, making it easy to share data between components at different levels of the component tree.
  • Cleaner Code: They encourage clean, modular code by separating data concerns from presentation concerns, resulting in more maintainable and understandable codebases.
  • Global State Management: Contexts excel at managing global state, ensuring that critical application data remains consistent and accessible throughout your entire app.
  • Improved Performance: By intelligently updating only the components that rely on changed data, contexts help optimize performance, reducing unnecessary re-renders.
  • Code Readability: Using contexts for state management enhances code readability, making it easier to grasp the structure and flow of your application.

Incorporating contexts into your React projects empowers you to build more efficient, maintainable, and scalable applications, ultimately leading to a better development experience and improved user satisfaction.


Pratical Case

The Challenge

Imagine we are building a weather application that displays the current weather conditions for different cities. Each city's weather data consists of its name, temperature, and weather description. This is the structure of our app :

Project architecture

Now, If we want to fetch data in SearchBar.tsx and display it in CurrentWeather.container.tx & WeekWeather.container.tsx without context we need to make a state at the top of our App.tsx :

1// App.tsx
2
3export type WeatherWeekData = {
4  temperature: number
5}
6
7export type WeatherData = {
8  town: string
9  current: WeatherWeekData
10  week: WeatherWeekData[]
11}
12
13function App() {
14  const [weatherData, setWeatherData] = useState<WeatherData | null>(null)
15
16  return (
17    <div className="App">
18      <Header />
19      <SearchBar setWeatherData={setWeatherData} />
20      {weatherData && <CurrentWeather town={weatherData.town} temperature={weatherData.current.temperature} />}
21      {weatherData && <WeekWeather town={weatherData.town} week={weatherData.week} />}
22      <Footer />
23    </div>
24  )
25}
26
27export default App
1// SearchBar.tsx
2
3type SearchBarProps = {
4  setWeatherData: (weatherData: WeatherData) => void
5}
6
7export const SearchBar = ({ setWeatherData }: SearchBarProps) => {
8  const [searchTerm, setSearchTerm] = useState<string>('')
9
10  const handleOnInputChange = (event: ChangeEvent<HTMLInputElement>) => {
11    // wait 1s delay before set the term and fetch data
12    setTimeout(() => {
13      setSearchTerm(event.target.value)
14    }, 1000)
15  }
16  const fetchData = (term: string) => {
17    fetch('https://mysuperAPI.com/search?term=' + term)
18      .then((response) => response.json())
19      .then((data) => setWeatherData(data))
20  }
21
22  useEffect(() => {
23    fetchData(searchTerm)
24  }, [searchTerm])
25
26  return (
27    <div>
28      <input placeholder="Find your town" value={searchTerm} onChange={handleOnInputChange} />
29    </div>
30  )
31}
1// CurrentWeather.container.tsx
2
3type CurrentWeatherProps = {
4  town: string
5  temperature: number
6}
7
8export const CurrentWeather = ({ town, temperature }: CurrentWeatherProps) => {
9  return (
10    <div>
11      <h1>Current Weather</h1>
12
13      <div>
14        <h2>{town}</h2>
15        <p>{temperature} °F</p>
16      </div>
17    </div>
18  )
19}
1// WeekWeather.container.tsx
2
3type WeekWeatherProps = {
4  town: string
5  week: WeatherWeekData[]
6}
7
8export const WeekWeather = ({ town, week }: WeekWeatherProps) => {
9  const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
10
11  return (
12    <div>
13      <h1>WeekWeather Weather</h1>
14
15      <div>
16        <h2>{town}</h2>
17
18        <div>
19          {week.map((day, index) => (
20            <div>
21              <h3>Day : {days[index]}</h3>
22              <p>{day.temperature} °F</p>
23            </div>
24          ))}
25        </div>
26      </div>
27    </div>
28  )
29}

As we can see, we need to share the state and the setState to make sure that other sister components have the data. Unfortunately, this solution is not maintainable. There are props all over the place, and the data is not really centralized.


Solution

In order to get a cleaner code, we're going to create a context! The purpose of this is to store data so that it can be accessed by all components.

Structure : Create a folder src/contexts/ and create 3 files in the context's folder like this :

Contexts architecture

Explainations

  • Weather actions : the list of our actions except GET : setWeather, setFavoriteTown, and more…
  • Weather provider : This will surround the part of your application that requires access to data. It also manages the state itself.
  • Weather reducer : He manages the various actions, so if you want to modify or add data, it's up to him.

Create all files :

Actions
1// weather.actions.ts
2
3import { WeatherData } from './weather.reducer'
4
5export enum EWeatherActions {
6  SET_WEATHER = 'SET_WEATHER'
7}
8
9type SetWeather = {
10  type: EWeatherActions.SET_WEATHER
11  payload: WeatherData
12}
13
14export const setWeather = (args: WeatherData): SetWeather => ({
15  type: EWeatherActions.SET_WEATHER,
16  payload: args
17})
18
19export type WeatherActions = SetWeather

Good to know:

  • setWeather is the action we should call if we want to add the data to our context.
  • We export the type to type our reducer.
Reducer
1// weather.reducer.ts
2
3import { Reducer } from 'react'
4import { EWeatherActions, WeatherActions } from './weather.actions'
5
6export type WeatherWeekData = {
7  temperature: number
8}
9
10export type WeatherData = {
11  town: string
12  current: WeatherWeekData | null
13  week: WeatherWeekData[]
14}
15
16export type WeatherState = {
17  weather: WeatherData | null
18}
19
20export const initialState: WeatherState = {
21  weather: null
22}
23
24export const weatherReducer: Reducer<WeatherState, WeatherActions> = (state = initialState, action) => {
25  switch (action.type) {
26    case EWeatherActions.SET_WEATHER:
27      return {
28        ...state,
29        ...action.payload
30      }
31    default:
32      return { ...state }
33  }
34}

Good to know:

  • There is a lot of type but the important thinks are initialState and weatherReducer.
  • initialState : As the name, is the initial state of our context. We juste put a weather object with our data.
  • weatherReducer : It's a simple switch / case by action type.
Provider
1// weather.provider.ts
2
3import { createContext, Dispatch, ReactNode, useContext, useMemo, useReducer } from 'react'
4import { initialState, weatherReducer, WeatherState } from './weather.reducer'
5import { WeatherActions } from './weather.actions'
6
7type WeatherContext = WeatherState & {
8  dispatch: Dispatch<WeatherActions>
9}
10
11const weatherContext = createContext<WeatherContext>({ ...initialState, dispatch: () => {} })
12
13export const useWeatherContext = () => useContext(weatherContext)
14
15type WeatherProviderProps = {
16  children: ReactNode
17}
18
19export const WeatherProvider = ({ children }: WeatherProviderProps) => {
20  const [state, dispatch] = useReducer(weatherReducer, initialState)
21
22  const value: WeatherContext = useMemo(() => ({ ...state, dispatch }), [state])
23  return <weatherContext.Provider value={value}>{children}</weatherContext.Provider>
24}

Good to know:

  • weathercontext : Not important variable, it's juste to make the WeatherProvider.
  • useWeatherContext : It's a nickname, a shortcut to call our 'useContext'
  • WeatherProvider : Our state, we need to surround the part of our app that requires data to limit access and increase performance.

Use our context!

Our new App.tsx:

1// App.tsx
2
3function App() {
4  return (
5    <div className="App">
6      <Header />
7      <WeatherProvider>
8        <SearchBar />
9        <CurrentWeather />
10        <WeekWeather />
11      </WeatherProvider>
12      <Footer />
13    </div>
14  )
15}
16
17export default App

We have removed all props and enclose the Weather part with WeatherProvider to share data with.

To set data:
1// SearchBar.tsx
2
3export const SearchBar = () => {
4  const { dispatch: dispatchWeather } = useWeatherContext()
5  const [searchTerm, setSearchTerm] = useState<string>('')
6
7  const handleOnInputChange = (event: ChangeEvent<HTMLInputElement>) => {
8    // wait 1s delay before set the term and fetch data
9    setTimeout(() => {
10      setSearchTerm(event.target.value)
11    }, 1000)
12  }
13  const fetchData = (term: string) => {
14    fetch('https://mysuperAPI.com/search?term=' + term)
15      .then((response) => response.json())
16      .then((data) => dispatchWeather(setWeather(data)))
17  }
18
19  useEffect(() => {
20    fetchData(searchTerm)
21  }, [searchTerm])
22
23  return (
24    <div>
25      <input placeholder="Find your town" value={searchTerm} onChange={handleOnInputChange} />
26    </div>
27  )
28}

In this file, we take dispatch from useWeatherContext. Dispatch is a function that allows you to use one of our defined actions. Here, we take dispatch and rename it dispatchWeather. Renaming the dispatch makes it easier to debug when we have lots of contexts and dispatches.

To use data:
1// CurrentWeather.container.tsx
2
3export const CurrentWeather = () => {
4  const { weather } = useWeatherContext()
5
6  if (!weather) return <div>Please select a town.</div>
7
8  return (
9    <div>
10      <h1>Current Weather</h1>
11
12      <div>
13        <h2>{weather.town}</h2>
14        <p>{weather.current.temperature} °F</p>
15      </div>
16    </div>
17  )
18}
1// WeekWeather.container.tsx
2
3export const WeekWeather = () => {
4  const { weather } = useWeatherContext()
5  const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
6
7  if (!weather) return <div>Please select a town.</div>
8
9  return (
10    <div>
11      <h1>WeekWeather Weather</h1>
12
13      <div>
14        <h2>{weather.town}</h2>
15
16        <div>
17          {weather.week.map((day, index) => (
18            <div>
19              <h3>Day : {days[index]}</h3>
20              <p>{day.temperature} °F</p>
21            </div>
22          ))}
23        </div>
24      </div>
25    </div>
26  )
27}

And that's it ! We have created and used our own clean context! Congratulations.


Going further

Once you've grasped the basics of React contexts, you can take your application development to the next level by exploring advanced topics :

  • localStorage with Contexts: Combine the power of contexts with localStorage to persist application state. This is particularly useful for maintaining user preferences, such as theme choices, user settings, or even the last state of a user's shopping cart. By linking contexts with localStorage, you ensure that user-specific data is retained between sessions. This enhances the user experience by providing continuity and personalization.
  • Integration with Redux: While React contexts are excellent for managing local component-level state, Redux is a robust state management library that excels at managing global state across your entire application. You can leverage both by using Redux for overarching application state and contexts for more specific, component-level state management. This hybrid approach provides the best of both worlds, allowing you to efficiently manage and share data between components while keeping a global state store for complex application-level data.
  • Testing and Debugging: Explore tools and techniques for testing and debugging applications that utilize contexts. Libraries like React Testing Library and Redux DevTools can be incredibly valuable in ensuring the reliability and performance of your code.

Conclusion

In summary, React contexts are crucial for efficient data sharing in your applications. They simplify code, manage global state effectively, and boost performance. In a practical example, we saw how using contexts can drastically clean up your code. By mastering contexts, you'll build more efficient, maintainable apps without losing your readers' interest.

If you enjoyed this tutorial, please consider following me for more helpful content. Your support is greatly appreciated! Thank you!

X _brdnicolas

See more articles