DEV Community

Cover image for State Management in React.Js
Omololu Emmanuel
Omololu Emmanuel

Posted on • Updated on

State Management in React.Js

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

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;
Enter fullscreen mode Exit fullscreen mode

useReducer

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;
Enter fullscreen mode Exit fullscreen mode

Context API + useState

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>
  );
};
Enter fullscreen mode Exit fullscreen mode
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>
);
Enter fullscreen mode Exit fullscreen mode
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;
Enter fullscreen mode Exit fullscreen mode

Context API + useReducer

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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

Top comments (5)

Collapse
 
therealarmani profile image
Armani Appolon

Great article! Keep up the learning and continue to share

Collapse
 
omohemma profile image
Omololu Emmanuel

Absolutely!
πŸ‘πŸΎπŸ‘πŸΎπŸ‘πŸΎ

Collapse
 
amslezak profile image
Andy Slezak

Look forward to the next article in the series. Would love to see others like zustand!

Collapse
 
omohemma profile image
Omololu Emmanuel

Great!
zustand seems to be new in the game.
I should also try that out.
πŸ‘πŸΎfor recommending.

Collapse
 
omohemma profile image
Omololu Emmanuel

Great Recommendations Luke πŸ‘πŸΎ