DEV Community

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

Posted on โ€ข Edited on

8 2

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

Sentry blog image

How I fixed 20 seconds of lag for every user in just 20 minutes.

Our AI agent was running 10-20 seconds slower than it should, impacting both our own developers and our early adopters. See how I used Sentry Profiling to fix it in record time.

Read more

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 ๐Ÿ‘๐Ÿพ

Image of Docusign

๐Ÿ› ๏ธ Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more