DEV Community

Iretos
Iretos

Posted on

Basic State Management in reactjs

React is one of the most popular libraries to create interfaces for the web. You can use it for many use cases, but it shines in high interactive applications. Therefore you must somehow handle your locale state. In this post I show the basic possibilities to handle state with react itself.

Use the useState-hook for local state

To handle state for one component you can use the useState-Hook. In our first example we use this hook to save the number of clicks the user did on a button. This is the example from the react docs. useState is a function that takes the initial value of the state and returns an array with two elements. The first element is the current state and the second element is a function to update the state. It’s best practice to use array destructuring to get two variables with meaningful names.

function CounterButton(){
    const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1);
  }

  return (
    <button onClick={handleClick}>
      Clicked {count} times
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

Button that counts it’s clicks. - useState

In the example we get the two variables count and setCount. With count we can show the number of clicks in the button. To update this number we use the setCount function when the button is clicked. Therefore we register an event handler with the onClick property on the button. When the button is clicked, this function will be called. Inside handleClick we use the current state with the count variable, increment it and save it with setCount. React notice the change, rerun our CounterButton component, but this times the count variable has a new value.

With useState you can handle local state inside one component fine. You can use the CounterButton component multiple times in your application and they handle their state for them self. But what can you do, if you want to know the clicks on all CounterButtons. If one button is clicked, the count value of all buttons should increase.

To get this behavior, you can lift the state next parent component of all the CounterButtons. In our example it is the App component. You can use the useState hook inside the App component and pass the count and onClick handler as props to the CounterButtons. Props (short for properties) are arguments passed to a component. You will get the props as first argument in the component function. We use object destructuring to get meaningful names. Inside the component you can use this variables like all other variables.

If one button is clicked, the value in the App component is updated and the value of both buttons will increase.

Lifting the state into the parent

function CounterButton({count, onClick}){
    return (
        <button onClick={onClick}>Clicked {count} times</button>
  );
}

function App(){
    const [count, setCount] = useState(0);

    function handleClick(){
        setCount(count + 1);
    }

    return (
        <div>
            <CounterButton count={count} onClick={handleClick}/>
            <CounterButton count={count} onClick={handleClick}/>
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Multiple Buttons - lifting state

Save input values with the useState-hook onChange

Another use-case for useState is the value of an input field. In this example we us the state (name) as value and update the state (setState) in all changes of the input field. Therefore we use the event of onChange and the value of the target. The target is the input field and the value of the target is the typed text. In our case the name of the user.

function App(){
    const [name, setName] = useState("");

    return (
        <div>
            <label>Name: <input type="text" name="name" value={name} onChange={e => setName(e.target.value)} /></label>
            <p>{name}</p>
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Show value of an input field. - useState with onChange

To see our example works, we show the name in the p tag.

Prefer computed state over multiple useStates

In the next example we have a input field for the temperature in °C and show the temperature in °C and °F. The example shows, that we doesn’t always need useState for state variables. We could also save the °F with useState, but it is best practice to compute state variables, if possible. The fahrenheitTemperature is called computed state. Using computed state is more maintainable, then using multiple useState.

function App(){
    const [celsiusTemperature, setCelsiusTemperature] = useState(0);
    const fahrenheitTemperature = celsiusToFahrenheit(celsiusTemperature);  

    return (
        <div>
            <label>Temperature °C: <input type="number" name="temperatureCelsius" value={celsiusTemperature} onChange={e => setCelsiusTemperature(e.target.value)}/></label>
            <hr/>
            <p>°C: {celsiusTemperature}</p>
            <p>*F: {fahrenheitTemperature}</p>
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Convert Celsius temperature from input into Fahrenheit - lifted state + computedState

Save an object with useState

The next examples shows two input fields and you can type temperature in °C or °F the other will always be updated as well. This time we use useState not with a single number, but with an object that contains the temperature value and the unit. The unit can be c for °C or f for °F.

In the onChange function we grab the value from e.target.value and pass it with the right unit to setTemperature. One temperature we get from the state, the other we compute from the temperature inside the state.

function App(){
    const [temperature, setTemperature] = useState({value: 0, unit: "c"});

    const temperatureCelsius = temperature.unit === "c" ? temperature.value : fahrenheitToCelsius(temperature.value);
    const temperatureFahrenheit = temperature.unit === "f" ? temperature.value : celsiusToFahrenheit(temperature.value);

    return (
        <div>
            <label>Temperature °C: <input type="number" name="temperatureCelsius" value={temperatureCelsius} onChange={e => setTemperature({value: e.target.value, unit: "c"})}/></label>
            <label>Temperature °F: <input type="number" name="temperatureFahrenheit" value={temperatureFahrenheit} onChange={e => setTemperature({value: e.target.value, unit: "f"})}/></label>
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

Convert Celsius and Fahrenheit temperatures into each other - useState with an object + computedState

Use useReducer for complex state

If we have more complex state, we can use the useReducer-hook. The useReducer-hook takes a reducer function as first argument and the initial state as second argument. It returns an array with two elements. The first element is the current state and the second argument is a dispatch function. The dispatch function is used to change the state, but it doesn’t take the new state but an action. The old state and the dispatched action is passed to the reducer and the reducer must return the new state.

In our example we have the two action ‘increase’ and ‘decrease’. An action doesn’t need to be a string. We could also use an object like {”type”: “increase”, “steps”: 10}. But for simplicity we only use a string. When a user clicks one of the buttons, we use the dispatch function with the action. Our reducer will be called with the old state and the action. We differantiate the action and increase or decrease the state and return the new state.

With the useReducer state it is possible the handle more complex state, because the developer doesn’t change the complete state, but only calls defined actions.

function reducer(state, action){
    switch(action){
        case 'increase':
            return state + 1;
        case 'decrease':
      return state - 1;
    default:
      throw new Error("unknown action: " + action);
    }
}

function App(){
    const [count, dispatch] = useReducer(reducer, 0);

    return (
        <div>
            <button onClick={() => dispatch("decrease")}>-</button>
            <span>{count}</span>
            <button onClick={() => dispatch('increase')}>+</button>
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

Define explicit actions with useReducer

“global” state with useContext

Sometimes we don’t need state for one component, but for multiple component in different nesting levels. Therefore we can use reacts useContext hook. With useContext we can save state and access it in all children, without passing props over and over again. UseContext is not always a good solution, but in few cases like theming or the current language it can be very useful.

In our example we use the context to save the current theme and change the style of a button depending on the theme. To use a context we must create it with reacts createContext function. To save a value within the context we use the component ThemeContext.Provider and pass the value “light”. All children of this component can access the value by using the useContext hook with the ThemeContext as first argument.

const ThemeContext = createContext("light");

const themeDefinitions = {
  light: {
    color: "#000",
    bgColor: "#fff"
  },
  dark: {
    color: "#fff",
    bgColor: "#000"
  }
}

function ThemedButton({children}){
  const theme = useContext(ThemeContext);
  const themeDefinition = themeDefinitions[theme];
  const style = {"color": themeDefinition.color, "backgroundColor": themeDefinition.bgColor, "border": "none", "padding": "0.5em 1em"};

  return <button style={style}>{children}</button>
}

export function App(props) {
  return (
    <ThemeContext.Provider value="light">
      <ThemedButton>Hello World</ThemedButton>
    </ThemeContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

global state with useContext

Combine useState and useContext to change “global” state

The value of the context in this example can not be changed. In the next example we combine useContext and useState to change the value of the context. The example contains the same ThemedButton but also a ThemeSwitcher. The theme is saved in the App component with useState and passed into the ThemeContext. The ThemeSwicher uses the passed props setTheme to change the value of the theme state, when the radio buttons get changed.

const ThemeContext = createContext("light");

const themes = {
  light: {
    color: "#000",
    bgColor: "#fff"
  },
  dark: {
    color: "#fff",
    bgColor: "#000"
  }
}

function ThemedButton({children}){
  const theme = useContext(ThemeContext);
  const themeDefinition = themes[theme];
  const style = {"color": themeDefinition.color, "backgroundColor": themeDefinition.bgColor, "border": "none", "padding": "0.5em 1em"};

  return <button style={style}>{children}</button>
}

function ThemeSwitcher({theme, setTheme}){
  return (
    <div>
    <label>Light: <input type="radio" name="theme" value="light" checked={theme === "light"} onChange={e => setTheme(e.target.value)}/></label>
    <label>Dark: <input type="radio" name="theme" value="dark" checked={theme === "dark"} onChange={e => setTheme(e.target.value)}/></label>
    </div>
  )
}

function App(props) {
  const [theme, setTheme] = useState("light");

  return (
    <ThemeContext.Provider value={theme}>
      <ThemeSwitcher theme={theme} setTheme={setTheme}/>
      <ThemedButton>Hello World</ThemedButton>
    </ThemeContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Change global state - combine useContext and useState

useContext best practice

It’s best practice to define the context in a separate file and only export necessary functions for the developer. In the example we create the context and wrap the ThemeContext.Provider in our own ThemeProvider. The ThemeProvider saves the theme with useState and let the developer access the theme and change it. The custom useTheme hook wraps the useContext hook and ensures that the ThemeProvider is used in a parent component or throws an error with an meaningful error message.

// theme-context
import { createContext, useState, useContext } from "react";

const ThemeContext = createContext({});

const ThemeProvider = function ({ children }) {
  const [theme, setTheme] = useState("light");

  const value = {
    theme,
    setLightTheme: () => setTheme("light"),
    setDarkTheme: () => setTheme("dark")
  };
  return (
    <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
  );
};

const useTheme = function () {
  const context = useContext(ThemeContext);
  if (context === undefined) {
    throw new Error("useTheme must be used within a ThemeProvider");
  }
  return context;
};

export { ThemeProvider, useTheme };
Enter fullscreen mode Exit fullscreen mode

To use the ThemeContext we use the ThemeProvider component inside our app. All children of the ThemeProvider can access the theme with the useTheme hook. In the ThemedButton we use it, to style the button. In the ThemeSwitcher we use the useTheme hook to access the theme and change it when the radio buttons are changed.

// app
import * as React from "react";
import { ThemeProvider, useTheme } from "./theme-context";

const themes = {
  light: {
    color: "#000",
    bgColor: "#fff"
  },
  dark: {
    color: "#fff",
    bgColor: "#000"
  }
};

function ThemedButton({ children }) {
  const { theme } = useTheme();
  const themeDefinition = themes[theme];
  const style = {
    color: themeDefinition.color,
    backgroundColor: themeDefinition.bgColor,
    border: "1px solid " + themeDefinition.color,
    padding: "0.5em 1em"
  };

  return <button style={style}>{children}</button>;
}

function ThemeSwitcher() {
  const { theme, setLightTheme, setDarkTheme } = useTheme();
  return (
    <div>
      <label>
        Light:{" "}
        <input
          type="radio"
          name="theme"
          value="light"
          checked={theme === "light"}
          onChange={(e) => setLightTheme()}
        />
      </label>
      <label>
        Dark:{" "}
        <input
          type="radio"
          name="theme"
          value="dark"
          checked={theme === "dark"}
          onChange={(e) => setDarkTheme()}
        />
      </label>
    </div>
  );
}

export default function App(props) {
  return (
    <ThemeProvider>
      <ThemeSwitcher />
      <ThemedButton>Hello World</ThemedButton>
    </ThemeProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

useContext best practice

Summary

  • Use useState for local state
  • Lift the state to the nearest parent, if multiple children must access it.
  • Try to avoid extra state with computed state
  • Use useReducer for more complex local state
  • Use useContext for “global” state. Global doesn’t necessary mean global for the complete application. It should be as local as possible.

further reading

Top comments (0)