@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.
- package/CHANGELOG.md +19 -0
- package/package.json +10 -10
- 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.
|
|
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.
|
|
13
|
-
"atom.io": "0.54.
|
|
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.
|
|
20
|
-
"@dprint/json": "0.
|
|
21
|
-
"@dprint/markdown": "0.
|
|
22
|
-
"@dprint/oxc": "0.49.
|
|
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.
|
|
38
|
-
"oxlint-tsgolint": "7.0.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
83
|
-
if (Error.isError(
|
|
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,
|
|
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,
|
|
112
|
-
|
|
113
|
-
|
|
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 =
|
|
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,
|
|
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
|
|
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
|
-
{
|
|
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
|
-
{
|
|
178
|
+
{todoList.error ? (
|
|
181
179
|
<div className="pfp signed-out" />
|
|
182
|
-
) :
|
|
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
|
-
{
|
|
212
|
+
{visibleKeys.map((todoKey) => (
|
|
207
213
|
<Todo key={todoKey} todoKey={todoKey} />
|
|
208
214
|
))}
|
|
209
215
|
<NewTodo />
|