Understanding State Management in React:  useState and useContext

Understanding State Management in React: useState and useContext

Featured on Hashnode

Have you ever marveled at how clicking a button on a website magically increments a counter or changes the button's color? That's the magic of state at play.

In this article, we will explore the exciting world of state management in React. Whether you're just starting out or already on your way to becoming an expert with React, this article will equip you with the essentials for state management in React.

To kick things off, let's demystify the meaning of 'state' in programming and react.

What is State?

State in programming refers to the value of an object stored in an application at any given moment. Imagine a basket in a shopping mall for example, at any point, it can hold a different item from the mall, state refers to the item in the basket. One key thing to note is that state can be held in variables, data structures, even a server (the baskets), and changes over time in response to user interaction or events.

What is State in React?

React state is an object in a react component's memory which stores data that may change over time, data that largely affects the behavior of the component in most cases. In simpler terms, the state is the component's memory, which changes due to user interaction or events that occur.

State changes due to user interaction makes a component and the entire app dynamic and interactive. For example, clicking a like button or typing in a controlled form would update a component's memory, which causes React to re-render the component and display the changes on the user interface.

To fully grasp how state works in react, let's picture the action of liking a post on a social media platform. Imagine a freshly posted photo; initially, the like count is set to zero, which is stored in the app's memory. When a user clicks the like button, this action is more than just a simple click. It initiates a change in the application's memory, specifically increasing the like count. Following this change, the component refreshes itself to display the updated, higher number of likes on the user interface.

State management in React started with class components in 2013 with this.state. Components could initialize state within their constructor and call this.setState to update the state value. Here's an example of how handling state in class-based components looks:


import React from 'react';
class Counter extends React.Component {
  constructor(props) {
    super(props); 
    this.state = {
      count: 0,
    };
    // Binding the 'incrementCount' method to 'this' to ensure it has the correct context when called
    this.incrementCount = this.incrementCount.bind(this);
  }
  incrementCount() {
    this.setState({
      count: this.state.count + 1, // Incrementing the 'count' state property by 1
    });
  }
  render() {
    return (
      <div>
        <p>{this.state.count}</p>
        <button onClick={this.incrementCount}> Increment </button>
      </div>
    );
  }
}
export default Counter;

In the component above, the Counter class component in React initializes with a state called count set to zero. It also renders a button Increment and display the count. When the Increment button is clicked, an event handler method incrementCount is called to update the state's count by one using this.setState. This triggers a re-render of the component, updating the displayed count on the user interface.

React's useState hook

In the constantly evolving world of React, this.state has been the cornerstone of state management in class components. But the era of functional components came, bringing with it react hooks. Enter useState, which has offered a sleek, easier approach for developers.

What makes useState stands out is it's simplicity and avoiding the often tricky this keyword which is a hurdle for many JavaScript developers. With useState, managing state-related logic becomes smoother, cutting through the complexity with a cleaner, more direct syntax. Below is a high-level view of how useState works.

import state from "library"
initialize functional Component(){
    //initialize state value, where updateState is a function
    const [state, updateState] = state("default state awaiting update")  
    return (
        //handle state updates on user interface
        updateState(state + 1)
    )
}

Let's look at a few examples below to understand React's useState hook better.

import { useState } from "react";

export default function App() {
  const [balance, setBalance] = useState(1000); //our balance is 1000

  return (
    <div>
      <p>Current balance: ${balance}</p>
      <button onClick={() => setBalance(balance + 100)}>Deposit $100</button>
      <button onClick={() => setBalance(balance - 100)}>Withdraw $100</button>
    </div>
  );
}

In the example above we're looking at a very minimalist view of a bank application where withdrawals and deposits are done. The first thing to do is import useState from the "react" library with import { useState } from "react"; at the top of the file. This is crucial to enable state management throughout the entire file. Next, we create the App component and set as a default export so it can be reused in other components. Within App, the useState hook initializes a state variable balance with a default value of 1000. The useState hook also provides a setBalance function that's used to update the value of balance. In the returned JSX, the current balance is displayed in a paragraph element, and two buttons are rendered: one to deposit and another to withdraw money. These buttons both have onClick event handlers which update the state by adding or subtracting 100. This example shows one piece of state being influenced by two buttons. Check out the code here!

Let's take a look at another example.

import { useState, useEffect } from "react";

export default function App() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const fetchData = async () => {
    setLoading(true);
    try {
      const response = await fetch(
        "https://jsonplaceholder.typicode.com/todos/"
      );
      const jsonData = await response.json();
      setData(jsonData);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  // Effect hook to fetch data on component mount
  useEffect(() => {
    fetchData();
  }, []); // Empty dependency array to run useEffect only once

  // Conditional rendering based on the state
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
      <div>
        {data && <p>{JSON.stringify(data)}</p>}
        <button onClick={fetchData}>Refresh Data</button>
      </div>  
  );
}

The code above defines a functional component App that fetches data from an Application Programming Interface (API) and handles the data-fetching state. Upon mounting, it uses the useEffect hook to call fetchData—an asynchronous function that sets the loading state to true, attempts to fetch data from a URL ("jsonplaceholder.typicode.com/todos"), parses the JavaScript Object Notation (JSON) response, and stores it in the data state using the setData function. If an error occurs, it's caught, and the error state is updated with the error message using the setError function. Lastly, whether the fetch is successful or fails, the loading state is set to false in the finally block. There's also a button to trigger a data refresh, invoking the fetchData function again when clicked, where you can see the loading state briefly before the UI is updated. Check out the code here!

useState and prop drilling

In React's component-based architecture, components can borrow state from their parents using props, which are essentially arguments passed into components. Think of it like handing down a family recipe book to a child—the child receives all the recipes (state) from their parents, and the grandchildren can receive the recipes from their parents. Let's understand this better with a practical example: picture we're building an app to display food choices, like hot or cold dishes. As we study the example, you'll see how props let child components inherit and display data from parent components.


import React,{useState} from "react"
import AllFood from "./components/AllFood"

export default function App() {
  const [foodTemp, setFoodTemp] = useState({
    hot: 'pizza',
    cold: 'ice cream'
  })
  return (
    <div className="App">
      <AllFood foodTemp={foodTemp} />
    </div>
  );
}

The App component initialized a state foodTemp with an object, it also has a child component AllFood that takes in the foodTemp state as a prop.

import React,{useState} from "react"

export default function Allfood(props) {
  const{foodTemp } = props
  return (
    <div className="AllFood">
      <HotFood foodTemp={foodTemp} />
      <ColdFood foodTemp={foodTemp} />
    </div>
  );
}

function HotFood(props) {
    const {foodTemp} = props
    return(
        <p>Hot Food Choice: {foodTemp.hot}</p>
    )
}

 function ColdFood(props) {
    const {foodTemp} = props
    return(
        <p>Cold Food Choice: {foodTemp.cold} </p>
    )
}

In the code above the AllFood component accepts the foodTemp prop. Note that it doesn't do anything with the prop, it only passes the prop to its children component which then makes use of the prop and prints a paragraph element with the prop information. With this example, if the foodTemp gets updated, the changes will reflect in all the child components. This process is known as prop drilling.

There is a caveat to prop drilling. If your app should be multiple components deep it would require passing the same prop through every single parent component to reach a component nested deep in the hierarchy, which isn't the best way to go about handling state. So, let us look at a better way to make state available throughout the app without having to pass the state value through props.

useContext Hook

In React, the Context feature is designed for the easy sharing of data across the entire application without the need to pass it through props. It's like setting up a global variable that any component in the app has access to.

All we have to do to get context working is to create a new instance of context and wrap the highest-level component that we want to have access to that piece of state inside the context element. Let's break down how this looks in practice with a high-level overview.

create context //import context from react and initialize
<Context> //wrap context around app component
    <App/> 
</Context>

Below is a refactored version of the previous app in which useState and prop drilling are replaced with useContext.

import React,{useState, createContext} from "react";
import AllFood from "./components/AllFood";

export const foodContext =  createContext(null)
export default function App() {
  const [foodTemp] = useState({
    hot: 'pizza',
    cold: 'ice cream'
  })
  return (
    <div className="App">
      <foodContext.Provider value={foodTemp}>
        <AllFood />
      </foodContext.Provider>
    </div>
  );
}

In the application, there's a main component named App, which contains a piece of state called foodTemp. This state is an object that holds two pieces of information: a 'hot' food item (pizza) and a 'cold' food item (ice cream).

The createContext hook is first imported from react at the top of the application. Next, it is invoked with a compulsory default value within the parenthesis (null in our case) and stored in a variable foodContext which is exported.

In the App component's render block, there's a foodContext.Provider. This is like a container that holds the foodTemp data. Any component inside this provider can access the foodTemp data easily. The AllFood component is wrapped inside this provider, which means inside AllFood, or any of its child components can access the 'pizza' and 'ice cream' data without needing to pass it down explicitly through props. Let us look at the AllFood component next.

import React,{ useContext} from "react";
import {foodContext} from "../App"

export default function Allfood() {
  return (
    <div className="AllFood">
      <HotFood  />
      <ColdFood  />
    </div>
  );
}

function HotFood() {
  const hotOrCold = useContext(foodContext);
  return(
    <p>Hot Food Choice: {hotOrCold.hot}</p>
    )
}

function ColdFood() {
  const hotOrCold = useContext(foodContext);
    return(
        <p>Cold Food Choice: {hotOrCold.cold} </p>  
    )
}

There are three components in the code above: we have AllFood, the parent component, with its children HotFood and ColdFood. The first thing to note is that the parent AllFood component has no prop or state-related information; instead, it only houses the children components, who in turn do not receive any props.

We will use the state we made available throughout the AllFood component tree. The HotFood and ColdFood components access the state by first importing the useContext hook from React at the top of the application. useContext plays an important role as it makes the object in the context available to the component by receiving the foodContext argument which also has to be imported.

const hotOrCold = useContext(foodContext) is the breakdown of how it works. Where useContext gets access to the context in the state of the parent element and stores the context value in the hotOrCold variable.

context value = {
    hot: 'pizza',
    cold: 'ice cream'
  }

The code in the Allfood component would print:

Conclusion

Hope you learnt something reading through and this article has equipped you with a new tool to better handle state in react to make your application both dynamic and interactive. Leave a reaction if this post was of help to you!

I am open to conversations on Web Dev and Technical Writing. Connect with me on Twitter, LinkedIn or check out my Github.