Skip to main content

Selectors 🧠

createSelector is gameplate's memoized derived-state helper. It's the same shape Reselect made famous, minus all the bytes.

Basic selector — single input

import { createSelector } from 'gameplate';

const visibleEnemies = createSelector(
(s: State) => s.enemies,
(enemies) => enemies.filter((e) => e.visible),
);

visibleEnemies(state); // computes
visibleEnemies(state); // memoized — returns the same reference

The combiner re-runs only when the input returns a new reference (Object.is comparison).

Combining inputs — Reselect-style

const targetedEnemies = createSelector(
[(s: State) => s.player, (s: State) => s.enemies] as const,
(player, enemies) => enemies.filter((e) => Math.hypot(e.x - player.x, e.y - player.y) < 100),
);

The combiner re-runs when any input's reference changes.

Use as const for input arrays.

That gives TypeScript the precise tuple type, so each combiner parameter is inferred correctly instead of widening to unknown.

When memoization helps (and when it doesn't)

A selector saves the combiner's work; it does not save the input getter's work. The inputs are read every call. So:

  • Expensive combiner, cheap inputs: big win. Filtering 1,000 enemies happens once per state change instead of once per frame.

  • ⚠️ Cheap combiner, expensive inputs: no win. The inputs run every time anyway.

  • Inputs return new references every call (e.g. (s) => ({ ...s.player })): worst case. The combiner re-runs every time and it has to allocate the same object every time. Don't do this — return existing references straight from state.

Selectors compose

const visibleEnemies = createSelector(
(s: State) => s.enemies,
(enemies) => enemies.filter((e) => e.visible),
);

const visibleEnemiesNearPlayer = createSelector(
[(s: State) => s.player, visibleEnemies] as const,
(player, enemies) => enemies.filter((e) => closeTo(player, e)),
);

Composition is just function composition — no special API needed.

Useful with React

If you're rendering the game's UI / HUD with React, a useSyncExternalStore selector reduces re-renders:

import { useSyncExternalStore } from 'react';

function useGameSelector<R>(game: Game<State, any>, select: Selector<State, R>): R {
return useSyncExternalStore(
game.subscribe,
() => select(game.state()),
() => select(game.state()),
);
}

// Later:
const score = useGameSelector(game, (s) => s.score);
// or
const enemyCount = useGameSelector(
game,
createSelector(
(s: State) => s.enemies,
(e) => e.length,
),
);

This is a 5-line user-land helper, not part of gameplate — because we're renderer-agnostic.

Tiny by design

createSelector is ~30 lines of source. There is no shallow comparison toggle, no input selectors with their own memoization, no resultEqualityCheck. If you want any of that, the source is short enough to fork.