How to use useReducer and useContext hooks together with Typescript in React

Introduction

Are your components complex with too many states and props?. It is time to use UseReducer and UseContext hooks now to simplify them and keep them clean.

npx create-react-app react-context-app --template typescript
# or
yarn create react-app react-context-app --template typescript
export interface GameState {
players: Player[];
gameName: string;
winner: Player | null;
gameStatus: Status;
}

export enum Status {
NotStarted = 'Not Started',
InProgress = 'In Progress',
Finished = 'Finished',
}

export interface Player {
name: string;
id: number;
status: Status;
value?: number;
}

export const initialGameState: GameState = {
players: [],
gameName: 'Game1',
winner: null,
gameStatus: Status.NotStarted,
};
export enum ActionType {
AddPlayer,
SetPlayerValue,
ResetGame,
}

export interface AddPlayer {
type: ActionType.AddPlayer;
payload: Player;
}

export interface SetPlayerValue {
type: ActionType.SetPlayerValue;
payload: { id: number; value: number };
}

export interface ResetGame {
type: ActionType.ResetGame;
}

export type GameActions = AddPlayer | SetPlayerValue | ResetGame;
export function gameReducer(state: GameState, action: GameActions): GameState {
switch (action.type) {
case ActionType.AddPlayer:
return { ...state, players: [action.payload, ...state.players] };
case ActionType.ResetGame:
return {
...initialGameState,
players: state.players.map((player) => ({
...player,
status: Status.NotStarted,
value: 0,
})),
};
case ActionType.SetPlayerValue:
let newState = {
...state,
players: state.players.map((player: Player) =>
player.id === action.payload.id
? {
...player,
value: action.payload.value,
status: Status.Finished,
}
: player
),
};

return {
...newState,
winner: getWinner(newState.players),
gameStatus: getGameStatus(newState),
};

default:
return state;
}
}

const getWinner = (players: Player[]): Player | null => {
let winnerValue = 0;
let winner = null;
players.forEach((player) => {
if (player.value && player.value > winnerValue) {
winner = player;
winnerValue = player.value || 0;
}
});
return winner;
};

const getGameStatus = (state: GameState): Status => {
const totalPlayers = state.players.length;
let numberOfPlayersPlayed = 0;
state.players.forEach((player) => {
if (player.status === Status.Finished) {
numberOfPlayersPlayed++;
}
});
if (numberOfPlayersPlayed === 0) {
return Status.NotStarted;
}
if (totalPlayers === numberOfPlayersPlayed) {
return Status.Finished;
}
return Status.InProgress;
};

// helper functions to simplify the caller
export const addPlayer = (player: Player): AddPlayer => ({
type: ActionType.AddPlayer,
payload: player,
});

export const setPlayerValue = (id: number, value: number): SetPlayerValue => ({
type: ActionType.SetPlayerValue,
payload: { id, value },
});

export const resetGame = (): ResetGame => ({
type: ActionType.ResetGame,
});
export const GameContext = React.createContext<{
state: GameState;
dispatch: React.Dispatch<GameActions>;
}>({
state: initialGameState,
dispatch: () => undefined,
});
export const Poker = () => {
const [state, dispatch] = useReducer(gameReducer, initialGameState);
return (
<GameContext.Provider value={{ state, dispatch }}>
<div className='Header'>
<header>
<p>React useReducer and useContext example Poker App</p>
</header>
</div>
<div className='ContentArea'>
<div className='LeftPanel'>
<PlayersList />
</div>
<div className='MainContentArea'>
<Players />
</div>
<div className='RightPanel'>
<GameStatus />
</div>
</div>
<div className='Footer'>
<AddPlayer />
</div>
</GameContext.Provider>
);
};
export const AddPlayer = () => {
const { dispatch } = useContext(GameContext);

const [playerName, setPlayerName] = useState('');

const handlePlayerNameChange = (event: ChangeEvent<HTMLInputElement>) => {
setPlayerName(event.target.value);
};

const handleSubmit = (event: FormEvent) => {
const player: Player = {
id: +new Date(),
name: playerName,
status: Status.NotStarted,
};
dispatch(addPlayer(player));
setPlayerName('');
event.preventDefault();
};
return (
<>
<h4>Add New Player</h4>
<form onSubmit={handleSubmit}>
<input
value={playerName}
type='text'
onChange={handlePlayerNameChange}
/>
<button type='submit' value='Submit' disabled={playerName === ''}>
Add
</button>
</form>
</>
);
};
export const PlayersList = () => {
const { state } = useContext(GameContext);
return (
<div className='PlayersList'>
<h4>Players</h4>
{state.players.map((player) => (
<label>{player.name}</label>
))}
</div>
);
};
export const Players = () => {
const { state, dispatch } = useContext(GameContext);
const playPlayer = (id: number) => {
const randomValue = Math.floor(Math.random() * 100);
dispatch(setPlayerValue(id, randomValue));
};
return (
<div>
<h4>Players Status</h4>
<div className='PlayersContainer'>
{state.players.map((player: Player) => (
<div key={player.id} className='Player'>
<label>Name: {player.name}</label>
<label>Status : {player.status}</label>
<label>Card Value: {player.value}</label>
<button
disabled={player.status !== Status.NotStarted}
onClick={() => playPlayer(player.id)}
>
Show Card
</button>
</div>
))}
</div>
</div>
);
};
export const GameStatus = () => {
const { state, dispatch } = useContext(GameContext);
return (
<div className='GameStatus'>
<div className='Status'>
<h4>Game Status</h4>
<label>{state.gameStatus}</label>
<button onClick={() => dispatch(resetGame())}>Start New Game</button>
</div>
<div className='Winner'>
{state.gameStatus === Status.InProgress && (
<label>
In Lead : {state.winner?.name} by {state.winner?.value}
</label>
)}
{state.gameStatus === Status.Finished && (
<label>
Winner: {state.winner?.name} by {state.winner?.value}
</label>
)}
</div>
</div>
);
};
Game screenshot

Conclusion

Hope you had fun creating useContext and useReducer hooks and playing the game. As you have seen, the components look a lot clean without too many props/state and easy to manage the state/actions using useContext hook.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store