selectorator
v5.0.0
Published
Simplified generator of reselect selectors
Readme
selectorator
selectorator is an abstraction API for creating selectors via reselect with
less boilerplate code.
Usage
import { createSelector } from 'selectorator';
interface State {
foo: {
bar: string;
};
baz: string;
}
const getBarBaz = createSelector<State>()(['foo.bar', 'baz'], (bar, baz) => `${bar} ${baz}`);
const state = {
foo: { bar: 'bar' },
baz: 'baz',
};
console.log(getBarBaz(state)); // "bar baz"Not a whole lot of magic here, just simplifying the creation of the "identity selectors" that reselect requires,
instead replacing them with a convenient and flexible syntax for retrieval of a nested property in the state object. See
selection types for more details. It should be noted the list of input selectors must be wrapped
in an array:
// not allowed
const getThing = createSelector<State>()('thing');
// works
const getThing = createSelector<State>()(['thing']);This is because it allows for narrow typing to flow throughout. This is also the reason for the curried approach, which you can get more details here. But because of that curried structure, it is a common practice to create an app- or state-specific selector:
import { createSelector } from 'selectorator';
import type { AppState } from './types';
export const createAppSelector = createSelector<AppState>();This avoids the boilerplate of passing the type and currying all uses across the app. Here is the example from the
reselect README modified to use selectorator which uses this to compose selectors:
import { createAppSelector } from './utils';
// subtotal built using simple method
const getSubtotal = createAppSelector(['shop.items'], (items) => items.reduce((sum, { value }) => sum + value, 0));
// tax built with simple method combined with other selector
const getTax = createAppSelector(
[getSubtotal, 'shop.taxPercent'],
(subtotal, taxPercent) => subtotal * (taxPercent / 100),
);
// total built entirely with other selectors
const getTotal = createAppSelector([getSubtotal, getTax], (subtotal, tax) => subtotal + tax);
// example state
const state = {
shop: {
taxPercent: 8,
items: [
{ name: 'apple', value: 1.2 },
{ name: 'orange', value: 0.95 },
],
},
};
console.log('subtotal: ', getSubtotal(state)); // 2.15
console.log('tax: ', getTax(state)); // 0.172
console.log('total: ', getTotal(state)); // {2.322}Selection types
For compatibility and compisition, passing a manual selector like in the upstream reselect library is supported.
const getThing = createSelector<State>()([(state) => state.thing]);In addition, selectorator uses pathington under-the-hood for path
parsing, and that can be leveraged to retrieve nested values:
- Pulls from state (the first argument passed):
string=>'foo[0].bar'(pulls fromstate.foo[0].bar)number=>0(pulls fromstate[0])Array=>['foo', 0, 'bar'](pulls fromstate.foo[0].bar)
If passing multiple parameters (e.g., selector(state, props)), you can leverage the object option wrapper to specify
which argument to select from.
const getThings = createSelector<State>()({
stateThing: 'thing',
propsThing: { argIndex: 1, path: 'thing' },
});Both manual and pathington path selection types are supported in object form.
Default selectors
To help minimize boilerplate, selectorator has default computation selectors for common use-cases.
Single item
For a selector that only retrieves a single state value, it returns that value.
interface {
nested: {
thing: string;
}
}
const getThing = createSelector<State>()(['nested.thing']);
// `getThing` has signature `(state: State) => string`Multiple items
If you want to retrieve multiple items, then you can use a structured selector.
interface {
nested: {
thing: string;
otherThing: number;
}
}
const getThing = createSelector<State>()({
thing: 'nested.thing',
otherThing: ['nested', 'otherThing'],
});
// `getThing` has signature `(state: State) => { thing: string; otherThing: string }`The reason a structured selector is used is because this automatically ascribes a name to the selected values, which is a much more natural consumption mechnanism than destructuring an array of results. It also allows for smaller diffs if selections are added / removed.
TypeScript
To support simple and fluid typing, a currying syntax is used to allow narrow typing for input selectors, as well as the
computation selector. When creating a selector that accepts multiple params, the state should be array of the input
types, e.g. createSelector<[State, number[], boolean]>(options).
import { createSelector } from 'selectorator';
interface State {
foo: {
bar: string;
};
baz: string;
}
interface Props {
quz: number[];
}
// `State` is input type
const getBarBaz = createSelector<State>()([({ foo }) => foo, 'baz'], (foo, baz) => `${foo.bar} ${baz}`);
// `(state) => state.foo` has type signature `(state: State) => string`
// `(foo, baz) => `${foo.bar} ${baz}` has type signature `(foo: { bar: string }, baz: string) => string`
// `getBarBaz() `has type signature: `(state: State) => string`;
// State and `Props` are type input types
const getBarBaz = createSelector<[State, Props]>(
[({ foo }) => foo, 'baz', 'baz', { path: ['quz', 0], argIndex: 1 }],
(foo, baz, quz) => [`${foo.bar} ${baz}`, quz] as const,
);
// `(state) => state.foo` has type signature `(state: State) => string`
// `(foo, baz, quz) => `[`${foo.bar} ${baz}`, quz] as const` has type signature
// `(foo: { bar: string }, baz: string, quz: number) => readonly [string, number]`
// `getBarBaz() `has type signature: `(state: State, props: Props) => readonly [string, number]`;If you pass a very wide type, or no type at all, any is used for all values.
// `any` is input type
const getBarBaz = createSelector<any>()([({ foo }) => foo, 'baz'], (foo, baz) => `${foo.bar} ${baz}`);
// `(state) => state.foo` has type signature `(state: any) => string`
// `(foo, baz) => `${foo.bar} ${baz}` has type signature `(foo: any, baz: string) => string`
// `getBarBaz() `has type signature: `(state: any) => string`;
// `unknown` is input type (either inferred when not passed, or can be explicitly passed)
const getBarBaz = createSelector()([({ foo }) => foo, 'baz'], (foo, baz) => `${foo.bar} ${baz}`);
// `(state) => state.foo` has type signature `(state: any) => string`
// `(foo, baz) => `${foo.bar} ${baz}` has type signature `(foo: any, baz: string) => string`
// `getBarBaz() `has type signature: `(state: any) => string`;The narrow typing is also available for structured selectors!
const getStucturedBarBaz = createSelector<[State, Props]>(
{
foo: ({ foo }) => foo,
baz: 'baz',
quz: { argIndex: 1, path: ['quz', 0] },
},
(foo, baz, quz) => [`${foo.bar} ${baz}`, quz] as const,
);
// getStructuredBarBaz() has type signature: (foo: { bar: string }, baz: string, quz: number) => readonly [string, number];Options
If desired for customization, you can pass through the
createSelectorCreator options in reselect
when creating the selector.
import { deepEqual } from 'fast-equals';
import { createSelector } from 'selectorator';
import type { AppState } from './types';
export const createAppSelector = createSelector<AppState>({
argsMemoizeOptoins: { resultEqualityCheck: deepEqual },
memoizeOptions: { resultEqualityCheck: deepEqual },
});