State Management in React.Js
Tags: react, programming, javascript, weeklyretro
Published: 2022-07-23 16:34:00

This week, I got myself playing around with **React Hooks** to grasp the underlying concepts on how it works, pros and tradeoffs.

I developed a simple scoreboard mini app for Liverpool FC versus Manchester United. Great idea right? 
Here are some highlights on how it went down.

**useState**
```javascript
import {useState} from 'react'
import './App.css';
import ManchesterUnited from './assets/images/manchester-united.png';
import LiverpoolFC from './assets/images/liverpool-fc.png';

function App() {
  const initialScoreBoard = { home: 0, away: 1 };
  const [scores, setScores] = useState(initialScoreBoard);

  const incrementScore = (team) => {
    team === 'home'
      ? setScores({ home: scores.home++, ...scores })
      : setScores({ away: scores.away++, ...scores });
  }

  const decrementScore = (team) => {
    if (team === 'home') {
      if (scores.home === 0) return;
      setScores({ home: scores.home--, ...scores })
    }

    if (team === 'away') {
      if (scores.away === 0) return;
      setScores({ away: scores.away--, ...scores });
    }
   };


  return (
    <div className="App">
      <div className="score-board">
        <img
          className=""
          width="180"
          height="240"
          src={LiverpoolFC}
          alt="Liverpool FC"
        />
        <h1>{scores.home}</h1>
        &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
        <h1>{scores.away}</h1>
        <img
          className=""
          width="240"
          height="240"
          src={ManchesterUnited}
          alt="Liverpool FC"
        />
      </div>
      <div>
        <button onClick={() => incrementScore('home')}>Goal!!!</button>
        <button onClick={() => decrementScore('home')}>Reverse Goal!!!</button>
        &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
        <button onClick={() => incrementScore('away')}>Goal!!!</button>
        <button onClick={() => decrementScore('away')}> Reverse Goal!!!</button>
      </div>
    </div>
  );
}

export default App;
```

**useReducer**
```javascript
import { useReducer } from 'react';
import './App.css';
import ManchesterUnited from './assets/images/manchester-united.png';
import LiverpoolFC from './assets/images/liverpool-fc.png';

function App() {
  const INITIAL_STATE = {
    scores: { home: 0, away: 1 },
  };

  const SCORE_ACTION_TYPES = {
    SET_SCORES: 'SET_SCORES',
  };

  const scoreBoardReducer = (state, action) => {
    const { type, payload } = action;

    switch (type) {
      case SCORE_ACTION_TYPES.SET_SCORES:
        return { scores: payload, ...state };
      default:
        throw new Error(`Invalid action ${type}`);
    }
  };

  const setScores = (scores) => {
    dispatch({ type: SCORE_ACTION_TYPES.SET_SCORES, payload: scores });
  };

  const [{ scores }, dispatch] = useReducer(scoreBoardReducer, INITIAL_STATE);

  const incrementScore = (team) => {
    team === 'home'
      ? setScores({ home: scores.home++, ...scores })
      : setScores({ away: scores.away++, ...scores });
  };

  const decrementScore = (team) => {
    if (team === 'home') {
      if (scores.home === 0) return;
      setScores({ home: scores.home--, ...scores });
    }

    if (team === 'away') {
      if (scores.away === 0) return;
      setScores({ away: scores.away--, ...scores });
    }
  };

  return (
    <div className="App">
      <h1>Score Board</h1>
      <div className="score-board">
        <img
          className=""
          width="180"
          height="240"
          src={LiverpoolFC}
          alt="Liverpool FC"
        />
        <h1>{scores.home}</h1>
        &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
        <h1>{scores.away}</h1>
        <img
          className=""
          width="240"
          height="240"
          src={ManchesterUnited}
          alt="Liverpool FC"
        />
      </div>
      <div>
        <button onClick={() => incrementScore('home')}>Goal!!!</button>
        <button onClick={() => decrementScore('home')}>Reverse Goal!!!</button>
        &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
        <button onClick={() => incrementScore('away')}>Goal!!!</button>
        <button onClick={() => decrementScore('away')}> Reverse Goal!!!</button>
      </div>
    </div>
  );
}

export default App;
```

**Context API + useState**

```javascript
import { useState, createContext } from 'react';

const initialScoreBoard = { home: 0, away: 1 };

/**
 * Create Context with default state
 */
export const ScoreContext = createContext({
  score: initialScoreBoard,
  incrementScore: () => null,
  decrementScore: () => null,
});

/**
 * Implement useState for state mgt
 * Expose useState to Context Provider for Accessibility
 * return Context Provider
 */
export const ScoreProvider = ({ children }) => {
  const [scores, setScores] = useState(initialScoreBoard);

  const incrementScore = (team) => {
    team === 'home'
      ? setScores({ home: scores.home++, ...scores })
      : setScores({ away: scores.away++, ...scores });
  };

  const decrementScore = (team) => {
    if (team === 'home') {
      if (scores.home === 0) return;
      setScores({ home: scores.home--, ...scores });
    }

    if (team === 'away') {
      if (scores.away === 0) return;
      setScores({ away: scores.away--, ...scores });
    }
  };

  const value = { scores, incrementScore, decrementScore };

  return (
    <ScoreContext.Provider value={value}>{children}</ScoreContext.Provider>
  );
};
```

```javascript
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { ScoreProvider } from './context/scores.context';
import './index.css';

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <ScoreProvider>
      <App />
    </ScoreProvider>
  </React.StrictMode>
);
```

```javascript
import { useContext } from 'react';
import './App.css';
import ManchesterUnited from './assets/images/manchester-united.png';
import LiverpoolFC from './assets/images/liverpool-fc.png';
import { ScoreContext } from './context/scores.context';

function App() {
  const { scores, incrementScore, decrementScore } = useContext(ScoreContext);

  return (
    <div className="App">
      <h1>Score Board</h1>
      <div className="score-board">
        <img
          className=""
          width="180"
          height="240"
          src={LiverpoolFC}
          alt="Liverpool FC"
        />
        <h1>{scores.home}</h1>
        &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
        <h1>{scores.away}</h1>
        <img
          className=""
          width="240"
          height="240"
          src={ManchesterUnited}
          alt="Liverpool FC"
        />
      </div>
      <div>
        <button onClick={() => incrementScore('home')}>Goal!!!</button>
        <button onClick={() => decrementScore('home')}>Reverse Goal!!!</button>
        &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
        <button onClick={() => incrementScore('away')}>Goal!!!</button>
        <button onClick={() => decrementScore('away')}> Reverse Goal!!!</button>
      </div>
    </div>
  );
}

export default App;
```
**Context API + useReducer**

```javascript
import { useReducer, createContext } from 'react';

const initialScoreBoard = { home: 0, away: 1 };

/**
 * Create Context with default state
 */
export const ScoreContext = createContext({
  scores: initialScoreBoard,
  incrementScore: () => null,
  decrementScore: () => null,
});

/**
 * Implement useState for state mgt
 * Expose useState to Context Provider for Accessibility
 * return Context Provider
 */
export const ScoreProvider = ({ children }) => {
  const INITIAL_STATE = {
    scores: { home: 0, away: 1 },
  };

  const SCORE_ACTION_TYPES = {
    SET_SCORES: 'SET_SCORES',
  };

  const scoreBoardReducer = (state, action) => {
    const { type, payload } = action;

    switch (type) {
      case SCORE_ACTION_TYPES.SET_SCORES:
        return { scores: payload, ...state };
      default:
        throw new Error(`Invalid action ${type}`);
    }
  };

  const setScores = (scores) => {
    dispatch({ type: SCORE_ACTION_TYPES.SET_SCORES, payload: scores });
  };

  const [{ scores }, dispatch] = useReducer(scoreBoardReducer, INITIAL_STATE);


  const incrementScore = (team) => {
    team === 'home'
      ? setScores({ home: scores.home++, ...scores })
      : setScores({ away: scores.away++, ...scores });
  };

  const decrementScore = (team) => {
    if (team === 'home') {
      if (scores.home === 0) return;
      setScores({ home: scores.home--, ...scores });
    }

    if (team === 'away') {
      if (scores.away === 0) return;
      setScores({ away: scores.away--, ...scores });
    }
  };

  const value = { scores, incrementScore, decrementScore };

  return (
    <ScoreContext.Provider value={value}>{children}</ScoreContext.Provider>
  );
};
```

My takeaway from this exercise is that context API gives a high level abstraction and make state accessible to parent component and its children leveraging DRY principle compared to useState and useReducer been implemented with the same logic in multiple components that need the state. 

useState and useReducer can be used independently of context API yet it fits perfectly into context API as state management grows.
It really an overkill to implement useReducer when all you have to mutate is a singular state value and also considering its verbose boilerplate. useReducer comes handy when an action mutate multiple state value e.g. in a cart system when you add an item to the cart - item(s) count, item(s) in cart, total cost of item(s) in cart, and possibly more depending on complexity.

**Link**: https://react-ts-dguw1i.stackblitz.io/

Thank you reading. what's your thought on this?

PS: I'll be adding redux and its additives to this post soon. Watch out for this space