Skip to content

Commit d5719c7

Browse files
committed
chore: refine usecases with all state handlers
1 parent 19b16cc commit d5719c7

37 files changed

Lines changed: 1503 additions & 383 deletions

usecases/basic/App.tsx

Lines changed: 139 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,180 @@
11
import { type FC } from 'react';
22
import { useDispatch } from 'react-redux';
3-
import { Layout, type UsecaseDescription } from '~usecases/lib/components/todo/Layout';
43

4+
import type { TransitionAction } from '~transitions';
5+
6+
import { ActivityFeed } from '~usecases/lib/components/activity/ActivityFeed';
7+
import { ProfileCard } from '~usecases/lib/components/profile/ProfileCard';
8+
import { ProjectBoard } from '~usecases/lib/components/projects/ProjectBoard';
9+
import { Layout, type UsecaseDescription } from '~usecases/lib/components/todo/Layout';
510
import { TodoApp } from '~usecases/lib/components/todo/TodoApp';
6-
import { createTodo, deleteTodo, editTodo } from '~usecases/lib/store/actions';
7-
import type { Todo } from '~usecases/lib/store/types';
11+
import { useAutoRetry } from '~usecases/lib/hooks/useAutoRetry';
12+
import { dismissActivity, editActivity, logActivity } from '~usecases/lib/store/activity/actions';
13+
import { createEpic, deleteEpic, editEpic } from '~usecases/lib/store/epics/actions';
14+
import { updateProfile } from '~usecases/lib/store/profile/actions';
15+
import { createProjectTodo, deleteProjectTodo, editProjectTodo } from '~usecases/lib/store/projects/actions';
16+
import type { ActivityEntry, Epic, Profile, ProjectTodo } from '~usecases/lib/store/types';
817
import { generateId, simulateAPIRequest } from '~usecases/lib/utils/mock-api';
918

10-
import { C, F, O, X } from '~usecases/lib/components/todo/CodeTags';
19+
import { C, F, O } from '~usecases/lib/components/todo/CodeTags';
1120

1221
const description: UsecaseDescription = {
13-
subtitle: 'Component-level async — the simplest optimistic pattern.',
22+
subtitle: 'Component-level async — full transition lifecycle managed in the component.',
1423
howItWorks: [
15-
<>Component dispatches <O>stage</O>, then awaits the API call.</>,
16-
<>On success: <O>amend</O> (server ID) → <C>commit</C>. On failure: <F>fail</F>.</>,
17-
<>Optimistic state is computed at the selector level — no state copies.</>,
18-
],
19-
tryIt: [
20-
<>Add a todo online — appears instantly (<O>opt</O>), then <C>commits</C>.</>,
21-
<>Toggle offline, add a todo — it <F>fails</F> with a jiggle.</>,
22-
<>Toggle back online — <F>failed</F> todos auto-retry.</>,
23-
<>Click "Sync API" — observe <X>conflict</X> detection.</>,
24+
<>Component dispatches <O>stage</O>, awaits the API, then <O>amend</O>s / <C>commit</C>s or <F>fail</F>s directly.</>,
25+
<>The full lifecycle (<O>stage</O> → API → <O>amend</O><C>commit</C> / <F>fail</F>) lives in the handler function — maximum visibility, minimum indirection.</>,
26+
<>Optimistic state is computed at the selector level via <code className="text-gray-400 text-[11px]">selectOptimistic</code> — no state copies, no checkpoints.</>,
27+
<>Retry routes the failed stage action back through the same handler — pattern matching on <code className="text-gray-400 text-[11px]">action.match()</code>.</>,
2428
],
2529
};
2630

2731
export const App: FC = () => {
2832
const dispatch = useDispatch();
2933

30-
const handleCreate = async (todo: Todo) => {
31-
const transitionId = todo.id;
34+
const handleCreateEpic = async (epic: Epic) => {
35+
const transitionId = epic.id;
36+
try {
37+
dispatch(createEpic.stage(epic));
38+
await simulateAPIRequest();
39+
dispatch(createEpic.amend(transitionId, { ...epic, id: generateId() }));
40+
dispatch(createEpic.commit(transitionId));
41+
} catch (error) {
42+
dispatch(createEpic.fail(transitionId, error));
43+
}
44+
};
45+
46+
const handleEditEpic = async (epic: Epic) => {
47+
const transitionId = epic.id;
48+
try {
49+
dispatch(editEpic.stage(epic.id, epic));
50+
await simulateAPIRequest();
51+
dispatch(editEpic.commit(transitionId));
52+
} catch (error) {
53+
dispatch(editEpic.fail(transitionId, error));
54+
}
55+
};
56+
57+
const handleDeleteEpic = async (epic: Epic) => {
58+
const transitionId = epic.id;
59+
try {
60+
dispatch(deleteEpic.stage(epic.id));
61+
await simulateAPIRequest();
62+
dispatch(deleteEpic.commit(transitionId));
63+
} catch (error) {
64+
dispatch(deleteEpic.stash(transitionId));
65+
}
66+
};
67+
68+
const handleUpdateProfile = async (update: Partial<Profile>) => {
69+
try {
70+
dispatch(updateProfile.stage(update));
71+
await simulateAPIRequest();
72+
dispatch(updateProfile.commit('profile'));
73+
} catch (error) {
74+
dispatch(updateProfile.fail('profile', error));
75+
}
76+
};
77+
78+
const handleCreateProjectTodo = async (todo: ProjectTodo) => {
79+
const transitionId = `${todo.projectId}/${todo.id}`;
80+
try {
81+
dispatch(createProjectTodo.stage(todo));
82+
await simulateAPIRequest();
83+
dispatch(createProjectTodo.amend(transitionId, { ...todo, id: generateId() }));
84+
dispatch(createProjectTodo.commit(transitionId));
85+
} catch (error) {
86+
dispatch(createProjectTodo.fail(transitionId, error));
87+
}
88+
};
3289

90+
const handleEditProjectTodo = async (todo: ProjectTodo) => {
91+
const transitionId = `${todo.projectId}/${todo.id}`;
3392
try {
34-
dispatch(createTodo.stage(todo));
93+
dispatch(editProjectTodo.stage(todo.projectId, todo.id, todo));
3594
await simulateAPIRequest();
95+
dispatch(editProjectTodo.commit(transitionId));
96+
} catch (error) {
97+
dispatch(editProjectTodo.fail(transitionId, error));
98+
}
99+
};
36100

37-
dispatch(createTodo.amend(transitionId, { ...todo, id: generateId() }));
38-
dispatch(createTodo.commit(transitionId));
101+
const handleDeleteProjectTodo = async (todo: ProjectTodo) => {
102+
const transitionId = `${todo.projectId}/${todo.id}`;
103+
try {
104+
dispatch(deleteProjectTodo.stage(todo.projectId, todo.id));
105+
await simulateAPIRequest();
106+
dispatch(deleteProjectTodo.commit(transitionId));
39107
} catch (error) {
40-
dispatch(createTodo.fail(transitionId, error));
108+
dispatch(deleteProjectTodo.stash(transitionId));
41109
}
42110
};
43111

44-
const handleEdit = async (todo: Todo) => {
45-
const transitionId = todo.id;
112+
const handleLogActivity = async (entry: ActivityEntry) => {
113+
const transitionId = entry.id;
114+
try {
115+
dispatch(logActivity.stage(entry));
116+
await simulateAPIRequest();
117+
dispatch(logActivity.amend(transitionId, { ...entry, id: generateId() }));
118+
dispatch(logActivity.commit(transitionId));
119+
} catch (error) {
120+
dispatch(logActivity.fail(transitionId, error));
121+
}
122+
};
46123

124+
const handleEditActivity = async (entry: ActivityEntry) => {
125+
const transitionId = entry.id;
47126
try {
48-
dispatch(editTodo.stage(todo.id, todo));
127+
dispatch(editActivity.stage(entry.id, entry));
49128
await simulateAPIRequest();
50-
dispatch(editTodo.commit(transitionId));
129+
dispatch(editActivity.commit(transitionId));
51130
} catch (error) {
52-
dispatch(editTodo.fail(transitionId, error));
131+
dispatch(editActivity.fail(transitionId, error));
53132
}
54133
};
55134

56-
const handleDelete = async (todo: Todo) => {
57-
const transitionId = todo.id;
135+
const handleDismissActivity = async (entry: ActivityEntry) => {
136+
const transitionId = entry.id;
58137
try {
59-
dispatch(deleteTodo.stage(todo.id));
138+
dispatch(dismissActivity.stage(entry.id));
60139
await simulateAPIRequest();
61-
dispatch(deleteTodo.commit(transitionId));
140+
dispatch(dismissActivity.commit(transitionId));
62141
} catch (error) {
63-
dispatch(deleteTodo.stash(transitionId));
142+
dispatch(dismissActivity.stash(transitionId));
64143
}
65144
};
66145

146+
/** Route a failed stage action through the correct lifecycle handler */
147+
const retryTransition = (action: TransitionAction) => {
148+
if (createEpic.stage.match(action)) return handleCreateEpic(action.payload.item);
149+
if (editEpic.stage.match(action)) return handleEditEpic(action.payload.item as Epic);
150+
if (updateProfile.stage.match(action)) return handleUpdateProfile(action.payload.item);
151+
if (createProjectTodo.stage.match(action)) return handleCreateProjectTodo(action.payload.item);
152+
if (editProjectTodo.stage.match(action)) return handleEditProjectTodo(action.payload.item as ProjectTodo);
153+
if (logActivity.stage.match(action)) return handleLogActivity(action.payload.item);
154+
if (editActivity.stage.match(action)) return handleEditActivity(action.payload.item as ActivityEntry);
155+
};
156+
157+
useAutoRetry(retryTransition);
158+
67159
return (
68160
<Layout title="Basic" description={description}>
69-
<TodoApp onCreateTodo={handleCreate} onEditTodo={handleEdit} onDeleteTodo={handleDelete} />
161+
<ProfileCard onUpdate={handleUpdateProfile} />
162+
<div className="grad-h my-1" />
163+
<TodoApp onCreateTodo={handleCreateEpic} onEditTodo={handleEditEpic} onDeleteTodo={handleDeleteEpic} onRetry={retryTransition} />
164+
<div className="grad-h my-1" />
165+
<ProjectBoard
166+
onCreateTodo={handleCreateProjectTodo}
167+
onEditTodo={handleEditProjectTodo}
168+
onDeleteTodo={handleDeleteProjectTodo}
169+
/>
170+
<div className="grad-h my-1" />
171+
<ActivityFeed
172+
onLogActivity={handleLogActivity}
173+
onEditActivity={handleEditActivity}
174+
onDismissActivity={handleDismissActivity}
175+
onRetry={retryTransition}
176+
/>
177+
<div className="h-4" />
70178
</Layout>
71179
);
72180
};

usecases/index.tsx

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -115,23 +115,34 @@ const Home: FC = () => (
115115

116116
<div className="text-left text-sm text-gray-400 leading-relaxed space-y-3 mb-8">
117117
<p>
118-
Optimistic state is <span className="text-gray-200">computed, not stored</span>.
119-
Only <code className="text-xs text-oc-stage/80 bg-surface-3 px-1 py-0.5 rounded">commit</code> mutates
120-
reducer state. All other operations modify the transitions list.
118+
This demo is a <span className="text-gray-200">project management app</span> built to showcase Optimistron's
119+
four state handlers — each section uses a different state shape with full optimistic CRUD.
121120
</p>
122121
<p>
123-
Think <code className="text-xs text-oc-stage/80 bg-surface-3 px-1 py-0.5 rounded">git rebase</code>
124-
committed state is your main branch, transitions are replayed on top at read-time.
122+
<code className="text-[10px] text-fuchsia-400 bg-surface-3 px-1 py-0.5 rounded">singularState</code>{' '}
123+
powers the user profile,{' '}
124+
<code className="text-[10px] text-amber-400 bg-surface-3 px-1 py-0.5 rounded">nestedRecordState</code>{' '}
125+
drives project-grouped tasks,{' '}
126+
<code className="text-[10px] text-cyan-400 bg-surface-3 px-1 py-0.5 rounded">recordState</code>{' '}
127+
backs the flat epic list, and{' '}
128+
<code className="text-[10px] text-green-400 bg-surface-3 px-1 py-0.5 rounded">listState</code>{' '}
129+
powers the activity log.
125130
</p>
126131
</div>
127132

128133
<div className="mb-8">
129134
<LifecycleSvg />
130135
</div>
131136

132-
<div className="p-3 rounded-lg bg-surface-3 text-xs text-gray-500 text-left leading-relaxed grad-wrap">
133-
Pick a usecase from the sidebar. Use the <span className="text-gray-300">Mock API</span> controls
134-
to simulate network conditions.
137+
<div className="p-3 rounded-lg bg-surface-3 text-xs text-gray-500 text-left leading-relaxed space-y-2 grad-wrap">
138+
<p>
139+
Pick a usecase from the sidebar. Each one implements the <span className="text-gray-300">same store</span> with
140+
a different async pattern — component-level, thunks, or sagas.
141+
</p>
142+
<p>
143+
Use the <span className="text-gray-300">Mock API</span> controls to toggle offline mode, adjust latency,
144+
and trigger a sync to see how Optimistron handles failures, retries, and conflict detection in real-time.
145+
</p>
135146
</div>
136147
</div>
137148
</div>
@@ -172,7 +183,7 @@ export const App: FC = () => (
172183
))}
173184
</nav>
174185

175-
<div className="h-48 flex-shrink-0 flex flex-col">
186+
<div className="h-36 flex-shrink-0 flex flex-col">
176187
<div className="grad-h" />
177188
<div className="px-5 pt-2.5 pb-4 flex-1">
178189
<MockApiControls />

0 commit comments

Comments
 (0)