React TS- How to Use React Contexts for State Management

brdnicolas

Nicolas B.

Posted on August 27, 2023

React TS- How to Use React Contexts for State Management

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 :

// App.tsx

export type WeatherWeekData = {
  temperature: number
}

export type WeatherData = {
  town: string
  current: WeatherWeekData
  week: WeatherWeekData[]
}

function App() {
  const [weatherData, setWeatherData] = useState<WeatherData | null>(null)

  return (
    <div className="App">
      <Header />
      <SearchBar setWeatherData={setWeatherData} />
      {weatherData && <CurrentWeather town={weatherData.town} temperature={weatherData.current.temperature} />}
      {weatherData && <WeekWeather town={weatherData.town} week={weatherData.week} />}
      <Footer />
    </div>
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode
// SearchBar.tsx

type SearchBarProps = {
  setWeatherData: (weatherData: WeatherData) => void
}

export const SearchBar = ({ setWeatherData }: SearchBarProps) => {
  const [searchTerm, setSearchTerm] = useState<string>('')

  const handleOnInputChange = (event: ChangeEvent<HTMLInputElement>) => {
    // wait 1s delay before set the term and fetch data
    setTimeout(() => {
      setSearchTerm(event.target.value)
    }, 1000)
  }
  const fetchData = (term: string) => {
    fetch('https://mysuperAPI.com/search?term=' + term)
      .then((response) => response.json())
      .then((data) => setWeatherData(data))
  }

  useEffect(() => {
    fetchData(searchTerm)
  }, [searchTerm])

  return (
    <div>
      <input placeholder="Find your town" value={searchTerm} onChange={handleOnInputChange} />
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode
// CurrentWeather.container.tsx

type CurrentWeatherProps = {
  town: string
  temperature: number
}

export const CurrentWeather = ({ town, temperature }: CurrentWeatherProps) => {
  return (
    <div>
      <h1>Current Weather</h1>

      <div>
        <h2>{town}</h2>
        <p>{temperature} °F</p>
      </div>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode
// WeekWeather.container.tsx

type WeekWeatherProps = {
  town: string
  week: WeatherWeekData[]
}

export const WeekWeather = ({ town, week }: WeekWeatherProps) => {
  const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']

  return (
    <div>
      <h1>WeekWeather Weather</h1>

      <div>
        <h2>{town}</h2>

        <div>
          {week.map((day, index) => (
            <div>
              <h3>Day : {days[index]}</h3>
              <p>{day.temperature} °F</p>
            </div>
          ))}
        </div>
      </div>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

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
// weather.actions.ts

import { WeatherData } from './weather.reducer'

export enum EWeatherActions {
  SET_WEATHER = 'SET_WEATHER'
}

type SetWeather = {
  type: EWeatherActions.SET_WEATHER
  payload: WeatherData
}

export const setWeather = (args: WeatherData): SetWeather => ({
  type: EWeatherActions.SET_WEATHER,
  payload: args
})

export type WeatherActions = SetWeather
Enter fullscreen mode Exit fullscreen mode

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
// weather.reducer.ts

import { Reducer } from 'react'
import { EWeatherActions, WeatherActions } from './weather.actions'

export type WeatherWeekData = {
  temperature: number
}

export type WeatherData = {
  town: string
  current: WeatherWeekData | null
  week: WeatherWeekData[]
}

export type WeatherState = {
  weather: WeatherData | null
}

export const initialState: WeatherState = {
  weather: null
}

export const weatherReducer: Reducer<WeatherState, WeatherActions> = (state = initialState, action) => {
  switch (action.type) {
    case EWeatherActions.SET_WEATHER:
      return {
        ...state,
        ...action.payload
      }
    default:
      return { ...state }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
// weather.provider.ts

import { createContext, Dispatch, ReactNode, useContext, useMemo, useReducer } from 'react'
import { initialState, weatherReducer, WeatherState } from './weather.reducer'
import { WeatherActions } from './weather.actions'

type WeatherContext = WeatherState & {
  dispatch: Dispatch<WeatherActions>
}

const weatherContext = createContext<WeatherContext>({ ...initialState, dispatch: () => {} })

export const useWeatherContext = () => useContext(weatherContext)

type WeatherProviderProps = {
  children: ReactNode
}

export const WeatherProvider = ({ children }: WeatherProviderProps) => {
  const [state, dispatch] = useReducer(weatherReducer, initialState)

  const value: WeatherContext = useMemo(() => ({ ...state, dispatch }), [state])
  return <weatherContext.Provider value={value}>{children}</weatherContext.Provider>
}
Enter fullscreen mode Exit fullscreen mode

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:

// App.tsx

function App() {
  return (
    <div className="App">
      <Header />
      <WeatherProvider>
        <SearchBar />
        <CurrentWeather />
        <WeekWeather />
      </WeatherProvider>
      <Footer />
    </div>
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode

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

To set data:
// SearchBar.tsx

export const SearchBar = () => {
  const { dispatch: dispatchWeather } = useWeatherContext()
  const [searchTerm, setSearchTerm] = useState<string>('')

  const handleOnInputChange = (event: ChangeEvent<HTMLInputElement>) => {
    // wait 1s delay before set the term and fetch data
    setTimeout(() => {
      setSearchTerm(event.target.value)
    }, 1000)
  }
  const fetchData = (term: string) => {
    fetch('https://mysuperAPI.com/search?term=' + term)
      .then((response) => response.json())
      .then((data) => dispatchWeather(setWeather(data)))
  }

  useEffect(() => {
    fetchData(searchTerm)
  }, [searchTerm])

  return (
    <div>
      <input placeholder="Find your town" value={searchTerm} onChange={handleOnInputChange} />
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

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:
// CurrentWeather.container.tsx

export const CurrentWeather = () => {
  const { weather } = useWeatherContext()

  if (!weather) return <div>Please select a town.</div>

  return (
    <div>
      <h1>Current Weather</h1>

      <div>
        <h2>{weather.town}</h2>
        <p>{weather.current.temperature} °F</p>
      </div>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode
// WeekWeather.container.tsx

export const WeekWeather = () => {
  const { weather } = useWeatherContext()
  const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']

  if (!weather) return <div>Please select a town.</div>

  return (
    <div>
      <h1>WeekWeather Weather</h1>

      <div>
        <h2>{weather.town}</h2>

        <div>
          {weather.week.map((day, index) => (
            <div>
              <h3>Day : {days[index]}</h3>
              <p>{day.temperature} °F</p>
            </div>
          ))}
        </div>
      </div>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

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!

💖 💪 🙅 🚩
brdnicolas
Nicolas B.

Posted on August 27, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related