@atom.io/template-react-node-backend 0.0.103 → 0.0.104

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/package.json +10 -10
  3. package/src/App.tsx +48 -42
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # @atom.io/template-react-node-backend
2
2
 
3
+ ## 0.0.104
4
+
5
+ ### Patch Changes
6
+
7
+ - 4b4c19a: Document and demonstrate synchronous atom effects with direct state writes as the place to normalize accepted remote responses, keeping hydration behind stale-result protection. Update loader, refresh, mutation, and local-index guidance; explain shared-record revisions and how to avoid extra requests. Move the React/Node template's todo hydration into an effect and add an explicit response refresh action.
8
+ - Updated dependencies [4b4c19a]
9
+ - Updated dependencies [90f634c]
10
+ - Updated dependencies [5f84875]
11
+ - Updated dependencies [b166670]
12
+ - Updated dependencies [ffbb22d]
13
+ - Updated dependencies [06c8a0c]
14
+ - Updated dependencies [90f634c]
15
+ - Updated dependencies [4349006]
16
+ - Updated dependencies [90f634c]
17
+ - Updated dependencies [8b43951]
18
+ - Updated dependencies [ccefaca]
19
+ - atom.io@0.54.6
20
+ - @atom.io/react@0.2.1
21
+
3
22
  ## 0.0.103
4
23
 
5
24
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atom.io/template-react-node-backend",
3
- "version": "0.0.103",
3
+ "version": "0.0.104",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/jeremybanka/atom.io.git",
@@ -9,17 +9,17 @@
9
9
  "license": "Apache-2.0",
10
10
  "type": "module",
11
11
  "dependencies": {
12
- "@atom.io/react": "0.2.0",
13
- "atom.io": "0.54.5",
12
+ "@atom.io/react": "0.2.1",
13
+ "atom.io": "0.54.6",
14
14
  "react": "19.3.1",
15
15
  "react-dom": "19.3.1",
16
16
  "zod": "4.6.7"
17
17
  },
18
18
  "devDependencies": {
19
- "@atom.io/eslint-plugin": "0.1.1",
20
- "@dprint/json": "0.25.2",
21
- "@dprint/markdown": "0.28.2",
22
- "@dprint/oxc": "0.49.0",
19
+ "@atom.io/eslint-plugin": "0.1.2",
20
+ "@dprint/json": "0.26.0",
21
+ "@dprint/markdown": "0.29.0",
22
+ "@dprint/oxc": "0.49.1",
23
23
  "@eslint-react/eslint-plugin": "5.28.8",
24
24
  "@types/node": "26.6.6",
25
25
  "@types/react": "19.3.0",
@@ -34,11 +34,11 @@
34
34
  "eslint-plugin-react-refresh": "0.5.7",
35
35
  "globals": "17.14.0",
36
36
  "oxc-transform-react": "0.157.0",
37
- "oxlint": "1.91.0",
38
- "oxlint-tsgolint": "7.0.2005",
37
+ "oxlint": "1.92.0",
38
+ "oxlint-tsgolint": "7.0.2006",
39
39
  "typescript": "npm:@typescript/typescript6@6.0.2",
40
40
  "typescript-eslint": "8.72.1",
41
- "vite": "8.3.5"
41
+ "vite": "8.3.6"
42
42
  },
43
43
  "publishConfig": {
44
44
  "access": "public"
package/src/App.tsx CHANGED
@@ -21,32 +21,37 @@ const todoSchema = z.object({
21
21
  })
22
22
  type Todo = z.infer<typeof todoSchema>
23
23
 
24
- /**
25
- * This atom tells us what todos exist.
26
- *
27
- * `Loadable` means that the value of this atom may be a `Promise` sometimes.
28
- *
29
- * `useLoadable` from `"@atom.io/react"` makes this easy to use in components.
30
- */
31
- const todoKeysAtom = atom<Loadable<number[]>, Error>({
24
+ /** Local membership also supports optimistic additions and deletions. */
25
+ const todoKeysAtom = atom<number[]>({
32
26
  key: `todoKeys`,
27
+ default: [],
28
+ })
29
+
30
+ /** Fetch a full response; normalize only after this loadable accepts it. */
31
+ const todoListAtom = atom<Loadable<Todo[]>, Error>({
32
+ key: `todoList`,
33
33
  default: async () => {
34
34
  const url = new URL(`/todos`, SERVER_URL)
35
35
  const response = await fetch(url, { credentials: `include` })
36
36
  if (!response.ok) throw new Error(response.status.toString())
37
37
  const json = await response.json()
38
- const todos = todoSchema.array().parse(json)
39
-
40
- /*
41
- 🚀 Performance hack!
42
- We can make our client less chatty by pre-populating our todo atoms.
43
- Comment out this line and watch the network tab to see the difference.
44
- */
45
- for (const todo of todos) setState(todoAtoms, todo.id, todo)
46
-
47
- return todos.map((todo) => todo.id)
38
+ return todoSchema.array().parse(json)
48
39
  },
49
40
  catch: [Error],
41
+ effects: [
42
+ ({ onSet }) => {
43
+ onSet(({ newValue }) => {
44
+ if (newValue instanceof Promise || Error.isError(newValue)) return
45
+ // Synchronous hydration reuses the list records without per-todo GETs.
46
+ // Superseded responses never reach this resolved-value callback.
47
+ for (const todo of newValue) setState(todoAtoms, todo.id, todo)
48
+ setState(
49
+ todoKeysAtom,
50
+ newValue.map((todo) => todo.id),
51
+ )
52
+ })
53
+ },
54
+ ],
50
55
  })
51
56
 
52
57
  /**
@@ -79,8 +84,9 @@ const todosStatsSelector = selector<
79
84
  >({
80
85
  key: `todosStats`,
81
86
  get: async ({ get }) => {
82
- const todoKeys = await get(todoKeysAtom)
83
- if (Error.isError(todoKeys)) return { total: 0, done: 0 }
87
+ const response = await get(todoListAtom)
88
+ if (Error.isError(response)) return { total: 0, done: 0 }
89
+ const todoKeys = get(todoKeysAtom)
84
90
  const total = todoKeys.length
85
91
  const todos = await Promise.all(todoKeys.map((id) => get(todoAtoms, id)))
86
92
  const done = todos.filter((todo) => !Error.isError(todo) && todo.done).length
@@ -94,11 +100,7 @@ async function addTodo() {
94
100
  const url = new URL(`todos`, SERVER_URL)
95
101
  const tempId = Math.random()
96
102
  setState(todoAtoms, tempId, { id: tempId, text, done: 0 })
97
- setState(todoKeysAtom, async (loadable) => {
98
- const prev = await loadable
99
- if (Error.isError(prev)) return prev
100
- return [...prev, tempId]
101
- })
103
+ setState(todoKeysAtom, (prev) => [...prev, tempId])
102
104
  const res = await fetch(url, {
103
105
  method: `POST`,
104
106
  credentials: `include`,
@@ -108,23 +110,16 @@ async function addTodo() {
108
110
  const todo = await res.json()
109
111
  const realId = todo.id
110
112
  setState(todoAtoms, realId, todo)
111
- setState(todoKeysAtom, async (loadable) => {
112
- const prev = await loadable
113
- if (Error.isError(prev)) return prev
114
- return prev.map((id) => (id === tempId ? realId : id))
115
- })
113
+ setState(todoKeysAtom, (prev) =>
114
+ prev.map((id) => (id === tempId ? realId : id)),
115
+ )
116
116
  }
117
117
 
118
118
  async function deleteTodo(todoKey: number) {
119
- const todoKeys = await getState(todoKeysAtom)
120
- if (Error.isError(todoKeys)) return
119
+ const todoKeys = getState(todoKeysAtom)
121
120
  const todoToDelete = todoKeys.find((key) => key === todoKey)
122
121
  if (todoToDelete === undefined) return
123
- setState(todoKeysAtom, async (loadable) => {
124
- const prev = await loadable
125
- if (Error.isError(prev)) return prev
126
- return prev.filter((id) => id !== todoToDelete)
127
- })
122
+ setState(todoKeysAtom, (prev) => prev.filter((id) => id !== todoToDelete))
128
123
  disposeState(todoAtoms, todoToDelete)
129
124
  const url = new URL(`/todos`, SERVER_URL)
130
125
  url.searchParams.set(`id`, todoToDelete.toString())
@@ -155,11 +150,14 @@ const SKELETON_KEYS = Array.from({ length: 5 }).map(Math.random)
155
150
  for (const KEY of SKELETON_KEYS) setState(todoAtoms, KEY, TODO_FALLBACK)
156
151
 
157
152
  export function App(): React.JSX.Element {
158
- const todoKeys = useLoadable(todoKeysAtom, SKELETON_KEYS)
153
+ const todoList = useLoadable(todoListAtom, [])
154
+ const todoKeys = useO(todoKeysAtom)
155
+ const visibleKeys =
156
+ todoList.loading && todoKeys.length === 0 ? SKELETON_KEYS : todoKeys
159
157
  const stats = useLoadable(todosStatsSelector, { total: 0, done: 0 })
160
158
  return (
161
159
  <>
162
- {todoKeys.error ? (
160
+ {todoList.error ? (
163
161
  <article className="takeover">
164
162
  <main className="card">
165
163
  <h1>Signed Out</h1>
@@ -177,9 +175,9 @@ export function App(): React.JSX.Element {
177
175
  <header>
178
176
  <LongLoadTimes />
179
177
  <span className="spacer" />
180
- {todoKeys.error ? (
178
+ {todoList.error ? (
181
179
  <div className="pfp signed-out" />
182
- ) : todoKeys.loading ? (
180
+ ) : todoList.loading ? (
183
181
  <div className="pfp loading" />
184
182
  ) : (
185
183
  <>
@@ -198,12 +196,20 @@ export function App(): React.JSX.Element {
198
196
  <main>
199
197
  <header>
200
198
  <h1>todo list</h1>
199
+ <button
200
+ type="button"
201
+ onClick={() => {
202
+ resetState(todoListAtom)
203
+ }}
204
+ >
205
+ Refresh
206
+ </button>
201
207
  <span>
202
208
  {stats.value.done}/{stats.value.total} items done
203
209
  </span>
204
210
  </header>
205
211
  <main>
206
- {todoKeys.value.map((todoKey) => (
212
+ {visibleKeys.map((todoKey) => (
207
213
  <Todo key={todoKey} todoKey={todoKey} />
208
214
  ))}
209
215
  <NewTodo />