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>
<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>
<button onClick={() => incrementScore('away')}>Goal!!!</button>
<button onClick={() => decrementScore('away')}> Reverse Goal!!!</button>
</div>
</div>
);
}
export default App;
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>
<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>
<button onClick={() => incrementScore('away')}>Goal!!!</button>
<button onClick={() => decrementScore('away')}> Reverse Goal!!!</button>
</div>
</div>
);
}
export default App;
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>
);
};
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>
);
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>
<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>
<button onClick={() => incrementScore('away')}>Goal!!!</button>
<button onClick={() => decrementScore('away')}> Reverse Goal!!!</button>
</div>
</div>
);
}
export default App;
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>
);
};
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)
Great article! Keep up the learning and continue to share
Absolutely!
๐๐พ๐๐พ๐๐พ
Look forward to the next article in the series. Would love to see others like zustand!
Great!
zustand seems to be new in the game.
I should also try that out.
๐๐พfor recommending.
Great Recommendations Luke ๐๐พ