Introduction to Redux-Saga
Redux-Saga is an intuitive side effect manager for Redux. There are many methods and tools to manage side effects with Redux, but Redux-Saga is fascinating because it’s an implementation of Communicating Sequential Processes, CSP for short. This might sound complex – even the wiki page may be slightly daunting! As part of this blog, we’re going to show how they can help improve readability, coordination, and isolation of logic compared to a more traditional React Redux app implementation.
Games
Games are interesting beasts and not your typical website or application. There are often several things that need to happen, and they are normally decoupled from the UI of the application. Traditional games are often made up of many independent systems that combine to provide visual output. React and Redux are not common choices for writing games, but for this example, we can show that using Redux-Saga will make this much more straightforward.
Blackjack
We’ll use Blackjack to explore how Redux-Saga can help with React game development. In case you’re unfamiliar, Blackjack is a card game where the goal is to try to finish with a higher total than the dealer without exceeding 21. If a player’s hand exceeds 21, then their hand is busted, and the player loses the game. There are some specific dealer rules beyond these, but that is the crux of the game.
Blackjack written in pseudo-code might look like the following:
- Create a shuffled deck of cards.
- Deal two cards to each player and two to the dealer.
- Calculate the total of each hand and check if any of the hands have Blackjack (21).
- When a player decides to hit, we will deal another card to the player and recalculate their total. The game will end if the player busts (goes over 21).
- Once all the players’ turns are complete, the dealer will continue to hit until they reach 17 or higher.
- Calculate the hand score of each and compare it to the dealer’s hand score to determine the result.
Taking the game to Sagas
Redux-Saga’s leverage generator functions to perform otherwise complex patterns, such as parallel execution, task concurrency, task racing, and task cancellation.
We start by breaking down our game into smaller sagas, which helps isolate logic based on its area of functionality. Sagas can also execute other sagas in two main modes – waiting for the Saga to complete, or forking the Saga to run as a “parallel” process.
Our Blackjack game has three main sagas: a “game” Saga for managing the flow of the entire game, a “turn” saga for managing the response to actions performed by a player in their turn, and finally, a “dealer” saga to perform the steps by the dealer within the rules of the Blackjack game.
Root Saga
The application’s Sagas are all set up by the initializing “root” saga, and the root saga is registered with redux in our application. As Redux-Saga is a middleware, this means creating the Redux-Saga middleware, registering middleware with Redux, and finally running the “root” Saga.
// create the saga middleware
const sagaMiddleware = createSagaMiddleware();
// configure the store using `@reduxjs/toolkit`
export const store = configureStore({
reducer,
// Add the sagaMiddleware to the middleware
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(sagaMiddleware)
});
// run the root saga
sagaMiddleware.run(rootSaga);
For our Blackjack game, the root saga brings all our game initialization together, starts a complete game, then resets and loops to allow playing the next new game.
export function* rootSaga(): SagaIterator {
while (true) {
// wait for the game to start
yield take(actions.start);
// get the number of players
const playerCount: number = yield select((state) => state.playerCount);
// add each player
for (let i = 0; i < playerCount; i++) {
yield put(actions.addPlayer());
}
// add the dealer
yield put(actions.addDealer());
// run the game
yield call(gameSaga);
// set the status to "ENDED."
yield put(actions.setStatus({ status: 'ENDED' }));
// wait for the reset action
yield take(actions.reset);
}
}
Game Saga
The game saga manages the blackjack game’s flow and logic. This Saga deals with shuffling the deck, performing the initial deal, executing the players’ and the dealer’s turns, and calculating the results.
function* gameSaga(): SagaIterator {
// Shuffle the deck for the game
yield call(shuffleDeckSaga);
// deal the initial cards to the players and the dealer
yield call(initialDealSaga);
// set status to playing
yield put(actions.setStatus({ status: 'PLAYING' }));
// select the dealer
const dealer: Player = yield select(getDealer);
// if the dealer has blackjack, skip to the results
if (dealer.result !== 'BLACKJACK') {
// select the players
const players: Player[] = yield select(getPlayers);
for (const player of players) {
// set the current player id
yield put(actions.setCurrentPlayer({ id: player.id }));
// if the player is a dealer, call the dealerSaga
if (isDealer(player)) {
yield call(dealerTurnSaga);
// if the player does not have Blackjack, call the player saga
} else if (player.result !== 'BLACKJACK') {
yield call(playerTurnSaga);
}
}
}
// calculate the results after all the players have finished
yield call(resultSaga);
}
Turn Sagas
Our game has two turn sagas, one for the players, playerTurnSaga
, and one for the dealer, dealerTurnSaga
.
The playerTurnSaga
listens and reacts to player interactions. In our example, this is hit
and stick
, with additional logic to detect other game scenarios, such as a player being busted with more than 21 points.
function* playerTurnSaga(): SagaIterator {
// Setup a channel that listens to the actions defined
const channel = yield actionChannel([actions.hit, actions.stick]);
while (true) {
// take action from the channel
const result = yield take(channel);
// if the action is "hit."
if (actions.hit.match(result)) {
// draw a card for the player
yield put(actions.draw());
// check if the player is bust
const bust = yield select(isBust);
if (bust) {
// if bust, set the result and exit turn
yield put(actions.setPlayerResult({ result: 'BUST' }));
break;
}
} else {
// otherwise "stick" and finish turn
break;
}
}
}
The dealerTurnSaga
manages the dealer’s behavior, with the game’s specific rules regarding when the dealer has to hit or stick. A notable difference between the playerTurnSaga
and the dealerTurnSaga
is that the dealerTurnSaga
does not react to an action on a channel; the dealerTurnSaga
is automated.
function* dealerTurnSaga(): SagaIterator {
while (true) {
// delay the dealer action by 1000ms
yield delay(1000);
// select the dealer
const dealer = yield select(getDealer);
// calculate the dealer score
const score = getHandScore(dealer.hand);
// if the score is less than 17, then draw a card
if (score < 17) {
yield put(actions.draw());
} else {
// if 17 or over, then check if the dealer is bust
const bust = yield select(isBust);
if (bust) {
// update the result if the dealer is bust
yield put(actions.setPlayerResult({ result: 'BUST' }));
}
// otherwise, stick on a score greater than 17
break;
}
}
}
The flow of the Sagas closely mirrors the flow of the high-level pseudo code, allowing the game logic to be easily understood. A complete example of our basic Blackjack implementation is available on Stackblitz or Github Repository. Here are some ideas to improve the game and learn more about redux-saga, left as exercises for the reader:
- Support Ace’s multiple values.
- Implement CPU players.
- Add Blackjack “split” functionality.
In Conclusion
As we have seen, Redux-Saga can be a radical shift in thinking for engineers. Therefore it is essential to determine whether Redux-Saga’s advanced side effects and flow management are required before using it in your project. For applications that have complicated flows that will benefit from features such as parallel execution, task concurrency, task racing, and task cancellation (such as a game), then Redux-Saga could be the right choice for your application.
SitePen used Redux-Saga to create an online version of Milestone Mayhem, a game dealing with software development’s trials and tribulations. For Milestone Mayhems’s game requirements, like the Blackjack example, Redux-Saga provided the flow management and allowed our engineers to decouple the game logic from the UI.