Skip to content

Commit f057991

Browse files
committed
chore: usecases with new exposed selectors
1 parent ca7cb6b commit f057991

14 files changed

Lines changed: 104 additions & 47 deletions

File tree

usecases/basic/App.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { type FC } from 'react';
2-
import { useDispatch } from 'react-redux';
2+
import { useDispatch, useStore } from 'react-redux';
33

44
import type { StagedAction } from '~transitions';
55

@@ -9,11 +9,16 @@ import { ProjectBoard } from '~usecases/lib/components/projects/ProjectBoard';
99
import { Layout, type UsecaseDescription } from '~usecases/lib/components/todo/Layout';
1010
import { TodoApp } from '~usecases/lib/components/todo/TodoApp';
1111
import { useAutoRetry } from '~usecases/lib/hooks/useAutoRetry';
12+
import { activitySelectors } from '~usecases/lib/store/activity/reducer';
1213
import { dismissActivity, editActivity, logActivity } from '~usecases/lib/store/activity/actions';
1314
import { createEpic, deleteEpic, editEpic } from '~usecases/lib/store/epics/actions';
15+
import { epicsSelectors } from '~usecases/lib/store/epics/reducer';
16+
import { profileSelectors } from '~usecases/lib/store/profile/reducer';
1417
import { updateProfile } from '~usecases/lib/store/profile/actions';
18+
import { projectsSelectors } from '~usecases/lib/store/projects/reducer';
1519
import { createProjectTodo, deleteProjectTodo, editProjectTodo } from '~usecases/lib/store/projects/actions';
1620
import type { ActivityEntry, Epic, Profile, ProjectTodo } from '~usecases/lib/store/types';
21+
import type { State } from '~usecases/lib/store/store';
1722
import { generateId, simulateAPIRequest } from '~usecases/lib/utils/mock-api';
1823

1924
import { C, F, O } from '~usecases/lib/components/todo/CodeTags';
@@ -144,14 +149,27 @@ export const App: FC = () => {
144149
};
145150

146151
/** Route failed transitions through the correct lifecycle handler on reconnect */
147-
useAutoRetry((action: StagedAction) => {
152+
const store = useStore<State>();
153+
154+
const retryAction = (action: StagedAction) => {
148155
if (createEpic.stage.match(action)) return handleCreateEpic(action.payload);
149156
if (editEpic.stage.match(action)) return handleEditEpic(action.payload as Epic);
150157
if (updateProfile.stage.match(action)) return handleUpdateProfile(action.payload);
151158
if (createProjectTodo.stage.match(action)) return handleCreateProjectTodo(action.payload);
152159
if (editProjectTodo.stage.match(action)) return handleEditProjectTodo(action.payload as ProjectTodo);
153160
if (logActivity.stage.match(action)) return handleLogActivity(action.payload);
154161
if (editActivity.stage.match(action)) return handleEditActivity(action.payload as ActivityEntry);
162+
};
163+
164+
useAutoRetry(() => {
165+
const state = store.getState();
166+
const failed = [
167+
...epicsSelectors.selectFailures(state.epics),
168+
...profileSelectors.selectFailures(state.profile),
169+
...projectsSelectors.selectFailures(state.projects),
170+
...activitySelectors.selectFailures(state.activity),
171+
];
172+
failed.forEach(retryAction);
155173
});
156174

157175
return (

usecases/lib/hooks/useAutoRetry.ts

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,17 @@
11
import { useEffect, useRef } from 'react';
2-
import { useSelector } from 'react-redux';
3-
4-
import type { StagedAction } from '~transitions';
52

63
import { useMockApi } from '~usecases/lib/components/mocks/MockApiProvider';
7-
import { selectAllFailedTransitions } from '~usecases/lib/store/epics/selectors';
84

9-
/** Retries all failed transitions when coming back online.
10-
* Only fires on the offline → online transition, not while already online.
11-
* The `retry` callback is pattern-specific:
12-
* - Basic/Thunks: routes each action to the correct handler/thunk
13-
* - Sagas: re-dispatches the stage action (saga watcher picks it up) */
14-
export const useAutoRetry = (retry: (action: StagedAction) => void) => {
5+
/** Calls `onReconnect` exactly once on the offline → online transition.
6+
* The retry strategy (what to dispatch) is the caller's concern. */
7+
export const useAutoRetry = (onReconnect: () => void) => {
158
const { online } = useMockApi();
16-
const failedTransitions = useSelector(selectAllFailedTransitions);
17-
const retryRef = useRef(retry);
18-
retryRef.current = retry;
19-
209
const wasOnline = useRef(online);
10+
const onReconnectRef = useRef(onReconnect);
11+
onReconnectRef.current = onReconnect;
2112

2213
useEffect(() => {
23-
if (online && !wasOnline.current) {
24-
failedTransitions.forEach((a) => retryRef.current(a));
25-
}
14+
if (online && !wasOnline.current) onReconnectRef.current();
2615
wasOnline.current = online;
27-
}, [online, failedTransitions]);
16+
}, [online]);
2817
};

usecases/lib/store/activity/reducer.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const compare = (a: ActivityEntry) => (b: ActivityEntry) => {
1313

1414
const eq = (a: ActivityEntry) => (b: ActivityEntry) => a.message === b.message && a.category === b.category;
1515

16-
export const { reducer: activity, selectOptimistic } = optimistron(
16+
const { reducer: activity, selectors } = optimistron(
1717
'activity',
1818
[] as ActivityEntry[],
1919
listState<ActivityEntry>({ key: 'id', compare, eq }),
@@ -30,3 +30,5 @@ export const { reducer: activity, selectOptimistic } = optimistron(
3030
},
3131
},
3232
);
33+
34+
export { activity, selectors as activitySelectors };

usecases/lib/store/activity/selectors.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { createSelector } from '@reduxjs/toolkit';
2-
import { selectIsConflicting, selectIsFailed, selectIsOptimistic } from '~selectors/selectors';
3-
import { selectOptimistic } from '~usecases/lib/store/activity/reducer';
2+
import { activitySelectors } from '~usecases/lib/store/activity/reducer';
43
import type { State } from '~usecases/lib/store/store';
54

5+
const { selectOptimistic, selectIsOptimistic, selectIsFailed, selectIsConflicting } = activitySelectors;
6+
67
export const selectOptimisticActivity = createSelector(
78
(state: State) => state.activity,
89
selectOptimistic((activity) => [...activity.state].sort((a, b) => b.timestamp - a.timestamp)),

usecases/lib/store/epics/reducer.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const compare = (a: Epic) => (b: Epic) => {
2222

2323
const eq = (a: Epic) => (b: Epic) => a.done === b.done && a.value === b.value;
2424

25-
export const { reducer: epics, selectOptimistic } = optimistron(
25+
const { reducer: epics, selectors } = optimistron(
2626
'epics',
2727
initial,
2828
recordState<Epic>({ key: 'id', compare, eq }),
@@ -44,3 +44,5 @@ export const { reducer: epics, selectOptimistic } = optimistron(
4444
},
4545
},
4646
);
47+
48+
export { epics, selectors as epicsSelectors };

usecases/lib/store/epics/selectors.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { createSelector } from '@reduxjs/toolkit';
2-
import { selectAllFailedTransitions as selectAllFailed, selectIsConflicting, selectIsFailed, selectIsOptimistic } from '~selectors/selectors';
3-
import { selectOptimistic } from '~usecases/lib/store/epics/reducer';
2+
import { epicsSelectors } from '~usecases/lib/store/epics/reducer';
43
import type { State } from '~usecases/lib/store/store';
54

5+
const { selectOptimistic, selectIsOptimistic, selectIsFailed, selectIsConflicting } = epicsSelectors;
6+
67
export const selectEpic = (id: string) =>
78
createSelector(
89
(state: State) => state.epics,
@@ -24,15 +25,6 @@ export const selectOptimisticEpicState = (id: string) =>
2425
}),
2526
);
2627

27-
/** All failed transitions across every slice — used by useAutoRetry */
28-
export const selectAllFailedTransitions = createSelector(
29-
(state: State) => state.epics,
30-
(state: State) => state.profile,
31-
(state: State) => state.projects,
32-
(state: State) => state.activity,
33-
(epics, profile, projects, activity) => selectAllFailed(epics, profile, projects, activity),
34-
);
35-
3628
/** Combined transitions from all reducers — feeds the transition graph */
3729
export const selectAllTransitions = createSelector(
3830
(state: State) => state.epics.transitions,

usecases/lib/store/profile/reducer.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const eq = (a: Profile) => (b: Profile) => a.displayName === b.displayName && a.
1515

1616
const initial: Profile = { displayName: 'Andy ZEN', avatarUrl: 'https://i.pravatar.cc/80?u=andy', revision: 0 };
1717

18-
export const { reducer: profile, selectOptimistic } = optimistron(
18+
const { reducer: profile, selectors } = optimistron(
1919
'profile',
2020
initial,
2121
singularState<Profile>({ compare, eq }),
@@ -32,3 +32,5 @@ export const { reducer: profile, selectOptimistic } = optimistron(
3232
},
3333
},
3434
);
35+
36+
export { profile, selectors as profileSelectors };

usecases/lib/store/profile/selectors.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { createSelector } from '@reduxjs/toolkit';
2-
import { selectIsConflicting, selectIsFailed, selectIsOptimistic } from '~selectors/selectors';
3-
import { selectOptimistic } from '~usecases/lib/store/profile/reducer';
2+
import { profileSelectors } from '~usecases/lib/store/profile/reducer';
43
import type { State } from '~usecases/lib/store/store';
54

5+
const { selectOptimistic, selectIsOptimistic, selectIsFailed, selectIsConflicting } = profileSelectors;
6+
67
export const selectOptimisticProfile = createSelector(
78
(state: State) => state.profile,
89
selectOptimistic((profile) => profile.state),

usecases/lib/store/projects/reducer.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const initial = (() => {
3131
return state;
3232
})();
3333

34-
export const { reducer: projects, selectOptimistic } = optimistron(
34+
const { reducer: projects, selectors } = optimistron(
3535
'projects',
3636
initial,
3737
nestedRecordState<ProjectTodo>()({ keys: ['projectId', 'id'], compare, eq }),
@@ -59,3 +59,5 @@ export const { reducer: projects, selectOptimistic } = optimistron(
5959
},
6060
},
6161
);
62+
63+
export { projects, selectors as projectsSelectors };

usecases/lib/store/projects/selectors.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { createSelector } from '@reduxjs/toolkit';
2-
import { selectIsConflicting, selectIsFailed, selectIsOptimistic } from '~selectors/selectors';
3-
import { selectOptimistic } from '~usecases/lib/store/projects/reducer';
2+
import { projectsSelectors } from '~usecases/lib/store/projects/reducer';
43
import type { State } from '~usecases/lib/store/store';
54
import type { ProjectTodo } from '~usecases/lib/store/types';
65

6+
const { selectOptimistic, selectIsOptimistic, selectIsFailed, selectIsConflicting } = projectsSelectors;
7+
78
export const selectOptimisticProjectTodos = (projectId: string) =>
89
createSelector(
910
(state: State) => state.projects,

0 commit comments

Comments
 (0)