Skip to main content

Organizing Producers

Everything you need to read and update your state can be contained within a single producer. However, as your game scales, you might prefer to split up your code into multiple files.

what you'll learn
  • 🎂 What a root producer is
  • 🍰 How to split producers into multiple slices
  • 📦 How to export your slices and its types
  • 📂 Creating a root producer

The root producer

The root producer is the entry point for your game's state and actions. You'll use it to perform all of your state updates and subscribe to your state.

In the Your First Producer guide, we created a producer called todos:

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

// ...

export const todos = createProducer(initialState, {
addTodo: (state, todo: string) => ({
...state,
todos: [...state.todos, todo],
}),

removeTodo: (state, todo: string) => ({
...state,
todos: state.todos.filter((t) => t !== todo),
}),
});

As our game grows, our state grows in complexity. For example, we might want to add a calendar to track upcoming events. Instead of repurposing our todos producer to include the calendar, it would be better to create slices.


Defining slices

A producer slice is a subset of your root producer's state and actions. Instead of being used on their own, they are used to define your root producer. By splitting up your producers into slices, you can keep your code organized and easy to maintain.

Here, both todos and calendar have been made into producer slices:

File structure
producer
├── calendar.ts
├── todos.ts
└── index.ts
todos.ts
import { createProducer } from "@rbxts/reflex";

export interface TodosState {
readonly todos: readonly string[];
}

const initialState: TodosState = {
todos: [],
};

export const todosSlice = createProducer(initialState, {
addTodo: (state, todo: string) => ({
...state,
todos: [...state.todos, todo],
}),

removeTodo: (state, todo: string) => ({
...state,
todos: state.todos.filter((t) => t !== todo),
}),
});

Our state has been broken into two slices:

  1. todosSlice manages a list of todos.
  2. calendarSlice tracks events on a calendar.

These slices can then be combined into a root producer.


Defining a root producer

The root producer file is where you'll combine all of your slices into a single producer. This file is the entry point for managing your game's state, and also exports some utility types to help us later. You can combine your slices with combineProducers:

index.ts
import { InferState, combineProducers } from "@rbxts/reflex";
import { todosSlice } from "./todos";
import { calendarSlice } from "./calendar";

export type RootProducer = typeof producer;

export type RootState = InferState<RootProducer>;

export const producer = combineProducers({
todos: todosSlice,
calendar: calendarSlice,
});

Now that we have a root producer, we can use the state and actions from our slices. Calling combineProducers does three things:

  1. Combine the state from each slice using the shape you provided.
  2. Expose the actions from each slice under the root producer.
  3. Merge any actions that have the same name.

With this, we can now access all of our state and actions from the root producer.

Using the root producer

As mentioned above, combining slices into a root producer exposes the actions from each slice under the root producer. This means that we can call the todo list's addTodo and removeTodo from the root producer, and it will update the state of todos:

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

const selectTodos = (state: RootState) => state.todos.todos;

producer.subscribe(selectTodos, (todos) => {
print(`TODO: ${todos.join(", ")}`);
});

producer.addTodo("Buy milk");
producer.addTodo("Buy eggs");
# TODO: Buy milk, Buy eggs

Or, we can call the calendar's addEvent and removeEvent from the root producer, and it will update the state of calendar:

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

const selectEvents = (state: RootState) => state.calendar.events;

producer.subscribe(selectEvents, (events) => {
for (const event of events) {
print(`${event.name} (${event.date})`);
}
});

producer.addEvent({ name: "Birthday", date: "2004-12-27" });
producer.addEvent({ name: "Learn Reflex", date: "2023-03-17" });
# Birthday (2004-12-27)
# Learn Reflex (2023-03-17)
caution

You should only call actions from the root producer. Calling actions from a slice will not update the state of the root producer, and vice versa.


Summary

  • The root producer is the entry point for your game's state.
  • Your actions and state are exposed under the root producer.
  • You can use slices to break up your state into smaller pieces.
  • You can combine slices into a root producer with combineProducers.