Skip to main content

createSelector

createSelector lets you skip re-computing a value if the inputs/dependencies haven't changed.

const selector = createSelector(...dependencies, combiner, options?);

Reference

createSelector(...dependencies, combiner, options?)

createSelector returns a memoized selector. This memoized selector will not call the combiner unless the arguments or the results of dependencies have changed. Calling this selector will pass the input arguments to each dependency, and then pass the results of those dependencies to the combiner.

The combiner function is called with the results of the dependencies as arguments, in the same order as the dependencies passed to createSelector. The result of the combiner is the final value that the selector will cache and return.

const selectArray = (state: State) => state.array;
const selectMap = (state: State) => state.map;

const selectValues = createSelector(selectArray, selectMap, (array, map) => {
return [...array, ...Object.values(map)];
});

Both the combiner and the dependencies are memoized. This means that the dependencies are not called unless the arguments to the selector have changed, and the combiner is not called unless the dependencies return new values.

See more examples below.

Parameters

  • ...dependencies - The selectors whose results will be passed to the combiner.
  • combiner - A function that takes the results of the dependencies and returns a value.
  • options - Optional settings to customize the behavior of the selector.

Returns

createSelector returns a memoized selector. It behaves as a normal selector, but the result is cached and only re-computed if the arguments or dependencies change.

Caveats
  • Your functions should be idempotent; they should always return the same result for the same inputs. Otherwise, the selector may not work as expected.

  • Dependencies are compared by reference (===). Even if a function returns an object that's shallowly equal to the previous result, it will still be seen as an update, and the selector will still re-compute.

  • If you're writing a selector returns a new object or array (i.e. filtering, sorting, etc.), you should always use createSelector. Otherwise, the selector will return a new object/array every time it's called, and listeners will run excessively. See the subscribe function for other caveats.


options argument

The options argument is an object that can be passed as the last argument to createSelector to customize its behavior.

  • options.equalityCheck? - A function that compares the current and previous values of a dependency. If false, the combiner will re-compute. By default, this is a strict equality check (===).
  • options.resultEqualityCheck? - Similar to options.equalityCheck, but it compares the current and previous results of the selector. If false, the selector will return the new result.

dependencies array

createSelector can also receive an array of dependencies instead of variadic arguments.

const selectValues = createSelector(selectArray, selectMap, (array, map) => {
return [...array, ...Object.values(map)];
});
Caveats
  • In Luau, using a dependency array may have worse type inference than using variadic arguments.

Usage

Transforming state

Often, you'll want to sort a list of items, filter out items that don't match a certain criteria, or perform some other transformation on your state. But trying to include these transformations in your state can quickly become difficult to keep up-to-date. Selectors are a great way to derive new values from state! With selectors, you can create a new object/array and apply transformations as needed.

But one small mistake can worsen performance or cause unexpected behavior. Let's say you have a list of items in your cart, and you want to filter out items that are out of stock. Your producer might look something like this:

interface CartState {
readonly items: readonly CartItem[];
}

interface CartItem {
readonly name: string;
readonly price: number;
readonly stock: number;
}

const initialState: CartState = {
items: [],
};

const producer = createProducer(initialState, {
addItem: (state, item: CartItem) => ({
...state,
items: [...state.items, item],
}),
});

You have a shopping cart that contains a list of items, each with a name, price, and stock property. How would you subscribe to state and filter out items that are in stock?

Your first instinct might be to write a simple function that filters items with a stock above zero:

const selectInStock = (state: CartState) => {
return state.items.filter((item) => item.stock > 0);
};

producer.subscribe(selectInStock, (stock) => {
print("Items available:", stock);
});

This works, but it's not very efficient. The listener will run every time the state updates, even if the items themselves haven't changed!

why?

By subscribing to state with a selector, you essentially tell Reflex to run your selector every time the state changes. This is to compare the result with the previous value to detect a change, which is fine for simple selectors, but it can be inefficient if your selector returns new tables and functions, or is expensive to compute.

In this case, every time the selector is called, it creates an entirely new list of in-stock items. This is bad because subscribe compares the new value to the previous value by reference (===), and since the selector only returns new arrays, Reflex will consider every unrelated state change to be an update. Your listener will be called excessively, even if the items haven't changed!

Memoization is a technique that allows you to cache the result of a function, and only re-compute it if the arguments change. createSelector offers a simple way to memoize selectors and avoid unnecessary re-computations, and is useful for writing efficient selectors that only run when needed.

We can wrap the selector in createSelector, and specify state.items as a dependency. With this change, the selector will only be called when state.items updates:

const selectItems = (state: CartState) => state.items;

const selectInStock = createSelector(selectItems, (items) => {
return items.filter((item) => item.stock > 0);
});

producer.subscribe(selectInStock, (stock) => {
print("Items available:", stock);
});

The selectInStock function is now memoized. In other words, it will only return a new list of items in-stock if state.items changed. This is much safer and more efficient than the previous example.

You'd pass two arguments to createSelector:

  1. An array of dependencies, which are selectors that the memoized selector depends on.
  2. A "combiner" function that transforms the dependencies into a new value.

createSelector then returns a new, memoized selector. Both the dependencies and the combiner function are memoized, so the selector will only re-compute if the selector arguments and the combiner dependencies change. There are three steps the selector takes to determine the return value:

  1. Make sure the arguments have changed since the last call.
  2. Call each dependency with the arguments and check if any of them changed.
  3. Call the combiner function with the new results of each dependency.

If all of the steps above pass, the selector will return the new value. Otherwise, it will return the previous value and skip the combiner function. The process is very fast, and helps keep your selectors efficient.

For a more in-depth explanation of selectors, check out the official Redux documentation. Most of the concepts apply to Reflex as well.


Passing input parameters

Common use cases for selectors are retrieving an item's state and applying search filters, but passing the external parameters needed to retrieve the information can be unintuitive at first.

We'll go over two ways to pass arguments, along with pros and cons:

  1. Selector factories: Pass arguments to a creator function, which returns a new selector. This is best used for creating selectors that are specialized for unique, unchanging parameters, like tracking an item's state by its ID.

  2. Dependency currying: Use dependencies to pass extra arguments to the combiner. This is best used for selectors that receive shared or frequently-changing parameters, like search queries or the sort order of a list.

Don't worry if you can't decide which method to use! Usually, selector factories are the safe choice.


Selector factories

Selector factories are functions that, given an initial set of arguments, return a new selector for those arguments. A unique instance of a selector with its own result cache is created for a given call to the factory.

It's best to use factories when selectors are created for a specific purpose. This can include:

  • Selecting the state of an item by ID. For example, you might want to play a damage sound when a specific player is hurt.

  • Deriving new values from a specific item. If you want to enhance an item with additional data, like a formatted price or a localized name, you can create a factory that takes an ID as an argument and returns a selector that enhances the item.

const selectItems = (state: CartState) => state.items;

const selectItemById = (id: number) => {
return createSelector(selectItems, (items) => {
return items.find((item) => item.id === id);
});
};

producer.subscribe(selectItemById(1), print);
producer.subscribe(selectItemById(2), print);
why?
  • Results are cached per selector. Two selectors created to select different user IDs will only trigger a state update if their respective users change. This is different from dependency currying, which has one level of caching and will re-compute if you use it with conflicting arguments.

  • Selectors can have state. You can assign logic to individual selectors, which can be used to store a history of state or other data.

why not?
  • You should memoize the selector in Roact. Be careful when using factories in Roact function components, as calling the factory without memoizing it will create a new selector every re-render, effectively forgetting the cache and causing excess re-renders. Reflex offers memoization for factories with the useSelectorCreator Hook.

  • Duplicate selectors can be wasteful. While selectors having their own caches is useful, creating lots of selectors with the same arguments is a bit wasteful. This leads to a few unnecessary re-computations, but it's usually not a problem unless it's expensive to compute or you have a lot of subscriptions.


Dependency currying

If your parameters can change often, and parameters do not conflict in your app, you can use dependency currying. The selector will have a single cache, and will re-compute if any of the parameters change.

This is especially effective for applying user input, like:

  • Filtering a list of items by a user's search query
  • Applying a sort direction to a list
  • Selecting a specific page of a paginated list
const selectItems = (state: CartState) => state.items.list;

const selectFilteredItems = createSelector(
selectItems,
(state: CartState, query: string) => query,
(items, query) => {
return items.filter((item) => {
const [match] = item.name.lower().match(query.lower());
return match !== undefined;
});
},
);

producer.getState((state) => selectFilteredItems(state, "query"));
why?
  • You can change parameters without creating a new selector. This is unlike selector factories, where each selector is created for a specific set of parameters.

  • It's safe to wrap in another function without memoizing it. You don't need any extra logic like useSelectorCreator to prevent creating new selectors and resetting the cache in Roact, but the syntax is a bit more verbose.

why not?
  • Avoid conflicting arguments for memoized selectors. Let's say you have two subscriptions to the selector that call it with different parameters. If the state changes, both subscriptions will call the selector with their own different parameters, and the selector will re-compute new values twice in every state change!

  • If you do have conflicting arguments, and your selector performs some transformation or expensive computation, selector factories will likely work better for you. You can prefer to use currying if your selector is simple and doesn't need createSelector, like indexing a property.