@jartur/solid-reducer v0.2.0
Solid Reducer
A simple reducer for SolidJS.
Inspired by React's useReducer hook, solid-reducer
is a simple reducer for SolidJS.
Installation
npm i @jartur/solid-reducer
yarn add @jartur/solid-reducer
pnpm add @jartur/solid-reducer
Usage
Without explicit types:
import { createReducer } from "@jartur/solid-reducer";
const [store, dispatch] = createReducer(
(set) => ({
increment: () => set("count", (c) => c + 1),
decrement: () => set("count", (c) => c - 1),
setText: (payload: string) => set("text", payload),
}),
{ count: 0, text: "" }
);
console.log(store); // { count: 0, text: "" }
dispatch("increment");
console.log(store); // { count: 1, text: "" }
dispatch("decrement");
console.log(store); // { count: 0, text: "" }
dispatch("setText", "foo");
console.log(store); // { count: 0, text: "foo" }
With explicit types:
import { Reducer, createReducer } from "@jartur/solid-reducer";
type Store = {
count: number;
text: string;
};
type ActionRecord = {
increment: void;
decrement: void;
setText: string;
};
const reducer: Reducer<Store, ActionRecord> = (set) => ({
increment: () => set("count", (c) => c + 1),
decrement: () => set("count", (c) => c - 1),
setText: (payload) => set("text", payload),
});
const [store, dispatch] = createReducer(reducer, { count: 0, text: "" });
console.log(store); // { count: 0, text: "" }
dispatch("increment");
console.log(store); // { count: 1, text: "" }
dispatch("decrement");
console.log(store); // { count: 0, text: "" }
dispatch("setText", "foo");
console.log(store); // { count: 0, text: "foo" }
API
createReducer
The createReducer
function takes a reducer function and an initial value and returns a store and a dispatch function.
function createReducer<Store extends object, ActionRecord>(
reducer: Reducer<Store, ActionRecord>,
initialValue: Store
): [Store<Store>, Dispatcher<ActionRecord>];
Reducer
The reducer function is a setup function that takes a setStore
function and a store
and returns an object with action handlers.
Action handlers are just functions that take a payload to update the store.
type Reducer<Store, ActionRecord> = (
setStore: SetStoreFunction<Store>,
store: Store
) => ActionMap<ActionRecord>;
type ActionMap<ActionRecord> = {
[Action in keyof ActionRecord]: void extends ActionRecord[Action]
? () => void
: (payload: ActionRecord[Action]) => void;
};
setStore and store
The setStore
function and the store
object are from solid-js/store
.
ActionRecord
The ActionRecord
type is a record that maps action types to their payload types. Actions that do not have a payload are mapped to void
.
dispatch
The dispatch
function receives an action type and a payload and calls the corresponding action handler.
If the ActionRecord defines a payload of type void
, there is no second argument.
export type DispatchFn<ActionRecord> = <ActionType extends keyof ActionRecord>(
type: ActionType,
...[payload]: void extends ActionRecord[ActionType]
? []
: [payload: ActionRecord[ActionType]]
) => void;
dispatch.subset
export type Dispatcher<ActionRecord> = DispatchFn<ActionRecord> & {
subset: <Action extends keyof ActionRecord>(
this: DispatchFn<ActionRecord>,
actions: Action[]
) => SubDispatcher<ActionRecord, Action>;
};
export type SubDispatcher<
ActionRecord,
Action extends keyof ActionRecord
> = DispatchFn<Pick<ActionRecord, Action>> & {
subset: <SubAction extends Action>(
this: DispatchFn<Pick<ActionRecord, Action>>,
actions: SubAction[]
) => SubDispatcher<ActionRecord, SubAction>;
};
The dispatch.subset
function is a helper function that accepts an array of action types and returns a dispatch function that only accepts this defined subset of actions.
This is useful for passing dispatch functions to components that should only be able to dispatch a subset of actions.
Passing an action type that is not defined in the subset throws an error at runtime and a typescript error at compile time.
const [store, dispatch] = createReducer(reducer, { count: 0, text: "" });
const dispatchCount = dispatch.subset(["increment", "decrement"]);
dispatchCount("increment");
dispatchCount("decrement");
dispatchCount("setText"); // throws Error: 'Action "setText" not allowed from this dispatcher'
// Typescript Error: 'Argument of type "setText" is not assignable to parameter of type "increment" | "decrement"'.
The dispatch.subset
function can be chained to create a subset from a subset with any depth.
const dispatchCount = dispatch.subset(["increment", "decrement"]);
const dispatchIncrement = dispatchCount.subset(["increment"]);
dispatchIncrement("increment");
dispatchIncrement("decrement"); // throws Error: 'Action "decrement" not allowed from this dispatcher'
// Typescript Error: 'Argument of type "decrement" is not assignable to parameter of type "increment"'.
If you want to pass a subset of actions to a component, you can use the subset
function in the component's setup
function.
const Component = () => {
const [store, dispatch] = createReducer(reducer, { count: 0, text: "" });
const dispatchCount = dispatch.subset(["increment", "decrement"]);
return <Child dispatch={dispatchCount} />;
};
const Child = (props: {
dispatch: SubDispatcher<ActionRecord, "increment" | "decrement">;
}) => {
return <button onClick={() => props.dispatch("increment")}>Increment</button>;
};
If you want the type-safety of a subset, but not the runtime implications, you can just assign the dispatch function to a variable with a SubDispatcher
type.
const [store, dispatch] = createReducer(reducer, { count: 0, text: "" });
const dispatchCount: SubDispatcher<ActionRecord, "increment" | "decrement"> =
dispatch;
dispatchIncrement("increment");
dispatchIncrement("decrement");
dispatchIncrement("setText"); // does not throw Error at runtime
// Typescript Error: 'Argument of type "setText" is not assignable to parameter of type "increment" | "decrement"'.
You can use the SubDispatcher
type on the props of a component to get type-safety and pass the full dispatch function directly.
const Component = () => {
const [store, dispatch] = createReducer(reducer, { count: 0, text: "" });
return <Child dispatch={dispatch} />;
};
const Child = (props: {
dispatch: SubDispatcher<ActionRecord, "increment" | "decrement">;
}) => {
return <button onClick={() => props.dispatch("increment")}>Increment</button>;
};
Store
Since createReducer
is a wrapper around createStore
, you can use produce
, reconcile
, unwrap
, and anything else you can do with a regular store.
import { Reducer, createReducer } from "@jartur/solid-reducer";
import { produce, unwrap } from "solid-js/store";
import { ActionRecord, Store } from "./types";
const reducer: Reducer<Store, ActionRecord> = (set) => ({
increment: () => set(produce((draft) => draft.count++)),
decrement: () => set(produce((draft) => draft.count--)),
setText: (payload) => set(produce((draft) => (draft.text = payload))),
});
const [store, dispatch] = createReducer(reducer, { count: 0, text: "" });
const data = unwrap(store);