Skip to main content

Subscribing to State

On Using Selectors, we learned how to use selectors to read and subscribe to state from the store. Reflex provides a few more useful ways to subscribe to state changes with selectors.

what you'll learn
  • 🌎 Common use cases for subscribing to state
  • 🔌 Different ways to subscribe to state
  • 📚 How to use subscribe, once, and wait

Player list​

Say your state had a players slice that stored the health of each player in a game:

players.ts
import { createProducer } from "@rbxts/reflex";

interface PlayersState {
readonly entities: {
readonly [id: string]: PlayerEntity;
};
}

export interface PlayerEntity {
readonly health: number;
}

const initialState: PlayersState = {
players: {},
};

export const playersSlice = createProducer(initialState, {
addPlayer: (state, id: string) => ({
...state,
players: { ...state.players, [id]: { health: 100 } },
}),

setPlayerHealth: (state, id: string, health: number) => ({
...state,
players: { ...state.players, [id]: { health } },
}),
});

The root state has a players slice with an entities field that contains your player entities. You will often need to run side effects for changes to state when working with entities. For example, you may want to play a sound when a player's health decreases.

Conditional side effects​

You can use subscribe to run side effects when a selector's value changes. In this example, we subscribe to the health of Player1 and play a sound when their health decreases:

import { RootState, producer } from "./producer";

const selectPlayerHealthById = (id: string) => {
return (state: RootState) => {
return state.players.entities[id].health;
};
};

const selectHealth = selectPlayerHealthById("Player1");

producer.subscribe(selectHealth, (health, lastHealth) => {
if (health < lastHealth) {
// Play sound
}
});

There are a few things to note about this example:

  • We defined selectPlayerHealthById, a selector factory, to create a simple selector that returns the health of a player with a given ID.
  • Our selector isn't memoized, but that's okay. It's not a problem since it's fast and returns a value directly from the state.
  • Our subscription callback receives the current value and the last value that the listener received. This is useful for comparing values and running conditional side effects.

As a shorthand, you can pass a predicate argument to subscribe to only run the listener when the predicate returns true. In this case, we can use it to only run the listener if the player's health is lower than the last health:

const didDecrease = (current: number, previous: number) => {
return current < previous;
};

producer.subscribe(selectHealth, didDecrease, (health, lastHealth) => {
// Play sound
});

One-time side effects​

You can use once to only run a listener the next time a value changes. If you want to end the game once the player's health reaches 0, you can use once:

const isDead = (health: number) => {
return health <= 0;
};

producer.once(selectHealth, isDead, () => {
// End game
});

Once the player's health reaches 0, the listener will be disconnected and the game will end.

Async side effects​

You can also use wait, which returns a Promise that resolves with the new value. This is especially useful for running side effects in async functions:

async function startGame() {
producer.setGameStatus("started");

return producer.wait(selectHealth, isDead).then(() => {
producer.setGameStatus("finished");
});
}

This uses wait to determine when to end the game, and can be chained off of to start the next game. You can apply the same logic to other side effects, like ending multiplayer matches or showing a game over screen.

Observing individual players​

You learned how to run side effects for one player, but what if you want to run individual side effects for each player? We will learn how to use observe in the next section.


Summary​

  • You can create conditional side effects by passing a predicate function.
  • subscribe runs a listener when a selector's value changes.
  • once runs a listener the next time a selector's value changes, then disconnects.
  • wait returns a Promise that resolves with the next value of a selector.