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:

  1. Create a shuffled deck of cards.
  2. Deal two cards to each player and two to the dealer. 
  3. Calculate the total of each hand and check if any of the hands have Blackjack (21).
  4. 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).
  5. Once all the players’ turns are complete, the dealer will continue to hit until they reach 17 or higher.
  6. 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.