React Note: State Management with Hooks

Before starting, it's important to note that the term state here refers to the application-level state, NOT component-level state.

Here, we use React's Hooks for state management, specifically useReducer() function. 

Most of state management code is contained inside a file named MyState.js. So, we're going to take a closer look at it first.

MyState.js

1. Inside MyState.js

Below is the code inside MyState.js. Read the accompanying comments for further information.

import React from "react";

// First, we use 'createContext()'. This will be used
// as <AppState.Provider /> to pass down the state
// to any of its child components.
export const AppState = React.createContext(null);

// Second, this is the initial state of our application.
// Here, we only store 'language' property, but you can
// add more properties.
//
// The 'language' propert will check your localStorage
// for existing value.
const initialState = {
  language: localStorage.getItem("lang") ? localStorage.getItem("lang") : "ID"
};

// Third, 'reducer()' is the function that will be used 
// to update the state.
//
// Here, the function must return a new state based on
// the 'action.type' parameter.
//
// For example, if 'action.type' === "language", it
// will store 'action.value' in localStorage and
// return a new state with the updated value.
const reducer = (state, action) => {
  switch (action.type) {
    case "language":
      localStorage.setItem("lang", action.value);
      return { language: action.value };
    default:
      throw new Error();
  }
}

// Lastly, this will be the final component that will
// be exported.
const MyState = (props) => {
  // Here, we use 'useReducer()' with the previously mentioned
  // 'reducer()' function and 'initialState' object.
  // It returns 'state' object and 'dispatch()' function
  //
  // * 'state' object is the current state object of the 
  //   application. Currently it contains the 'language'
  //   property.
  // * 'dispatch()' function is the function that will be used
  //   to update the current state.
  const [state, dispatch] = React.useReducer(reducer, initialState);

  // Here, we use the 'AppState' created from the first step.
  // The 'value' props contains the 'dispatch()' function and
  // 'state' object that will be passed down to its children.
  return (
    <AppState.Provider value={ {dispatch: dispatch, state: state} }>
      {children}
    </AppState.Provider>
  );
}

export default MyState;

2. Where to put <MyState />?

You can put <MyState /> in the top-level component such as App.js as follows:

import React from "react";
import { MyRouter } from "./routes";
import { MyTheme } from "./themes";
import { MyState } from "./state";

const App = () => {
  return (
    <MyState>
      <MyTheme>
        <MyRouter />
      </MyTheme>
    </MyState>
  );
}

export default App;

3. How to get and update current state from children components?

To get and update the current state, you must use useContext() function first. Then you will get state object to get current state and the dispatch() function to update them. 

Here's a simple example:

import React from "react";
// Import MyState.
import { AppState } from "./path/to/MyState";

const ExamplePage = () => {
  // Use useContext() function.
  const { state, dispatch } = React.useContext(AppState);

  const handleClickButton = () => {
    const action = {
      type: "language", 
      value: "EN"
    };
    // Call 'dispatch()' function when button is clicked
    // to change the language.
    dispatch(action);
  }

  // Use 'state' object to get current language.
  return (
    <p>Current Language: {state.language}</p>
    <button onClick={handleClickButton}>
      Change Language to EN
    </button>
  )
}

4. How to add more property to the state?

The example given above only uses one property, which is language

To add more property, just do the following. Let's say that you want to add theme property. 

// MyState.js

// Add 'theme' to initialState
const initialState = {
  language: localStorage.getItem("lang") ? localStorage.getItem("lang") : "ID",
  theme: localStorage.getItem("theme") ? localStorage.getItem("theme") : "light",
};

// Add 'theme' to 'reducer()'.
// Notice that the return state must include
// '...state' (spread operator) for unchanged
// properties.
const reducer = (state, action) => {
  switch (action.type) {
    case "language":
      localStorage.setItem("lang", action.value);
      return { 
        ...state,
        language: action.value,
      };
    case "theme":
      localStorage.setItem("theme", action.value);
      return { 
        ...state,
        theme: action.value,
      };
    default:
      throw new Error();
  }
}

That's it. Now you can get and update theme property from the current state just like the language property:

import React from "react";
import { AppState } from "./path/to/MyState";

const ExamplePage = () => {
  const { state, dispatch } = React.useContext(AppState);

  const handleClickButton = () => {
    const action = {
      type: "theme", 
      value: "dark"
    };
    dispatch(action);
  }

  return (
    <p>Current Theme: {state.theme}</p>
    <button onClick={handleClickButton}>
      Change Theme to dark
    </button>
  )
}