Eric Bower
·
17 Aug 24
store.md
1---
2title: Store
3description: An immutable store that acts like a reactive, in-memory database
4---
5
6Features:
7
8- A single, global javascript object
9- Reactive
10- Normalized
11- Acts like a database
12
13We love `redux`. We know it gets sniped for having too much boilerplate when
14alternatives like `zustand` and `react-query` exist that cut through the
15ceremony of managing state. However, `redux` was never designed to be easy to
16use; it was designed to be scalable, debuggable, and maintainable. Yes, setting
17up a `redux` store is work, but that is in an effort to serve its
18maintainability.
19
20Having said that, the core abstraction in `redux` is a reducer. Reducers were
21originally designed to contain isolated business logic for updating sections of
22state (also known as state slices). They were also designed to make it easier to
23sustain state immutability.
24
25Fast forward to `redux-toolkit` and we have `createSlice` which leverages
26`immer` under-the-hood to ensure immutability. So we no longer need reducers for
27immutability.
28
29Further, we argue, placing the business logic for updating state inside reducers
30(via switch-cases) makes understanding business logic harder. Instead of having
31a single function that updates X state slices, we have X functions (reducers)
32that we need to piece together in our heads to understand what is being updated
33when an action is dispatched.
34
35With all of this in mind, `starfx` takes all the good parts of `redux` and
36removes the need for reducers entirely. We still have a single state object that
37contains everything from API data, UX, and a way to create memoized functions
38(e.g. [selectors](/selectors)). We maintain immutability (using
39[immer](https://github.com/immerjs/immer)) and also have a middleware system to
40extend it.
41
42Finally, we bring the utility of creating a schema (like [zod](https://zod.dev)
43or a traditional database) to make it plainly obvious what the state shape looks
44like as well as reusable utilities to make it easy to update and query state.
45
46This gets us closer to treating our store like a traditional database while
47still being flexible for our needs on the FE.
48
49```ts
50import { createSchema, createStore, select, slice } from "starfx";
51
52interface User {
53 id: string;
54 name: string;
55}
56
57// app-wide database for ui, api data, or anything that needs reactivity
58const [schema, initialState] = createSchema({
59 cache: slice.table(),
60 loaders: slice.loaders(),
61 users: slice.table<User>(),
62});
63type WebState = typeof initialState;
64
65// just a normal endpoint
66const fetchUsers = api.get<never, User[]>(
67 "/users",
68 function* (ctx, next) {
69 // make the http request
70 yield* next();
71
72 // ctx.json is a Result type that either contains the http response
73 // json data or an error
74 if (!ctx.json.ok) {
75 return;
76 }
77
78 const { value } = ctx.json;
79 const users = value.reduce<Record<string, User>>((acc, user) => {
80 acc[user.id] = user;
81 return acc;
82 }, {});
83
84 // update the store and trigger a re-render in react
85 yield* schema.update(schema.users.add(users));
86
87 // User[]
88 const users = yield* select(schema.users.selectTableAsList);
89 // User
90 const user = yield* select(
91 (state) => schema.users.selectById(state, { id: "1" }),
92 );
93 },
94);
95
96const store = createStore(schema);
97store.run(api.register);
98store.dispatch(fetchUsers());
99```
100
101# How to update state
102
103There are **three** ways to update state, each with varying degrees of type
104safety:
105
106```ts
107import { updateStore } from "starfx";
108
109function*() {
110 // good types
111 yield* schema.update([/* ... */]);
112 // no types
113 yield* updateStore([/* ... */]);
114}
115
116store.run(function*() {
117 // no types
118 yield* store.update([/* ... */]);
119});
120```
121
122`schema.update` has the highest type safety because it knows your state shape.
123The other methods are more generic and the user will have to provide types to
124them manually.
125
126# Updater function
127
128`schema.update` expects one or many state updater functions. An updater function
129receives the state as a function parameter. Any mutations to the `state`
130parameter will be applied to the app's state using
131[immer](https://github.com/immerjs/immer).
132
133```ts
134type StoreUpdater<S extends AnyState> = (s: S) => S | void;
135```
136
137> It is highly recommended you read immer's doc on
138> [update patterns](https://immerjs.github.io/immer/update-patterns) because
139> there are limitations to understand.
140
141Here's a simple updater function that increments a counter:
142
143```ts
144function* inc() {
145 yield* schema.update((state) => {
146 state.counter += 1;
147 });
148}
149```
150
151Since the `update` function accepts an array, it's important to know that we
152just run those functions by iterating through that array.
153
154In fact, our store's core state management can _essentially_ be reduced to this:
155
156```ts
157import { produce } from "immer";
158
159function createStore(initialState = {}) {
160 let state = initialState;
161
162 function update(updaters) {
163 const nextState = produce(state, (draft) => {
164 updaters.forEach((updater) => updater(draft));
165 });
166 state = nextState;
167 }
168
169 return {
170 getState: () => state,
171 update,
172 };
173}
174```
175
176# Updating state from view
177
178You cannot directly update state from the view, users can only manipulate state
179from a thunk, endpoint, or a delimited continuation.
180
181This is a design decision that forces everything to route through our
182[controllers](/controllers).
183
184However, it is very easy to create a controller to do simple tasks like updating
185state:
186
187```ts
188import type { StoreUpdater } from "starfx";
189
190const updater = thunks.create<StoreUpdater[]>("update", function* (ctx, next) {
191 yield* updateStore(ctx.payload);
192 yield* next();
193});
194
195store.dispatch(
196 updater([
197 schema.users.add({ [user1.id]: user }),
198 ]),
199);
200```