Skip to content

Commit 0a7b2b5

Browse files
author
Nich Secord
committed
feat(SyncExternalStore): Add StatefulSyncExternalStore to extend and provide structure
1 parent 663bfb5 commit 0a7b2b5

File tree

7 files changed

+121
-0
lines changed

7 files changed

+121
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [Unreleased]
99

1010
- Added `ISyncExternalStore<T>` and `SyncExternalStore<T>` to make creating external stores for `React.useSyncExternalStore` easier (Requires React@18 or higher)
11+
- Add `StatefulSyncExternalStore` to provide structured extension of `SyncExternalStore`.
1112

1213
## [1.1.1] - 2022-10-13
1314

src/index.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
} from "./context-store--stateful-indexable/index.js";
77
import { getNotImplementedPromise } from "./shared/index.js";
88
export * from "./sync-external-store/index.js";
9+
export * from "./sync-external-store--stateful-store/index.js";
910

1011
export {
1112
ContextStore,

src/shared/index.ts

+16
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,19 @@ export const errorMessages = {
1616
};
1717

1818
export const getNotImplementedPromise = () => Promise.reject("Not Implemented");
19+
20+
export function normalizeError(error: unknown): null | string {
21+
if (error == null) {
22+
return null;
23+
}
24+
25+
if (typeof error === "string") {
26+
return error;
27+
}
28+
29+
if (error instanceof Error) {
30+
return error.message;
31+
}
32+
33+
return JSON.stringify(error);
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { normalizeError } from "../shared/index.js";
2+
import { StoreState } from "./store-item.js";
3+
4+
export type CreateLoadingSnapshot = typeof createLoadingSnapshot;
5+
export function createLoadingSnapshot<TStoreData extends StoreState>(key: keyof TStoreData) {
6+
return (snapshot: TStoreData) => {
7+
return {
8+
...snapshot,
9+
[key]: {
10+
...snapshot[key],
11+
state: "loading",
12+
},
13+
};
14+
};
15+
}
16+
17+
export type CreateErrorSnapshot = typeof createErrorSnapshot;
18+
export function createErrorSnapshot<TStoreData extends StoreState>(key: keyof TStoreData, error: unknown) {
19+
const errorMessage = normalizeError(error);
20+
return (snapshot: TStoreData) => {
21+
return {
22+
...snapshot,
23+
[key]: {
24+
...snapshot[key],
25+
error: errorMessage,
26+
state: "error",
27+
},
28+
};
29+
};
30+
}
31+
32+
export type CreateSuccessSnapshot = typeof createSuccessSnapshot;
33+
export function createSuccessSnapshot<TStoreData extends StoreState, TData = unknown>(
34+
key: keyof TStoreData,
35+
data: TData,
36+
) {
37+
return (snapshot: TStoreData) => {
38+
return {
39+
...snapshot,
40+
[key]: {
41+
...snapshot[key],
42+
data,
43+
state: "success",
44+
},
45+
};
46+
};
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from "./create-snapshot.js";
2+
export * from "./store-item.js";
3+
export * from "./store.js";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { statefulStates } from "../shared/index.js";
2+
3+
export type StoreState = Record<string, StoreItem<unknown>>;
4+
5+
export type StoreItem<TData> = {
6+
data: TData;
7+
error: null | string;
8+
state: typeof statefulStates;
9+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { SyncExternalStore } from "../sync-external-store/index.js";
2+
import {
3+
CreateErrorSnapshot,
4+
CreateLoadingSnapshot,
5+
CreateSuccessSnapshot,
6+
createErrorSnapshot,
7+
createLoadingSnapshot,
8+
createSuccessSnapshot,
9+
} from "./create-snapshot.js";
10+
import { StoreState } from "./store-item.js";
11+
12+
type UpdateStoreParams<TStore extends StoreState, TParams = void, TResponse = void> = {
13+
key: keyof TStore;
14+
params?: TParams;
15+
service: (params: TParams) => Promise<TResponse>;
16+
updateSnapshot?: {
17+
loading: CreateLoadingSnapshot;
18+
success: CreateSuccessSnapshot;
19+
error: CreateErrorSnapshot;
20+
};
21+
};
22+
23+
export abstract class StatefulSyncExternalStore<TStore extends StoreState> extends SyncExternalStore<TStore> {
24+
async updateStore<TParams, TResponse>(params: UpdateStoreParams<TStore, TParams, TResponse>) {
25+
const {
26+
key,
27+
params: serviceParams,
28+
service,
29+
updateSnapshot = {
30+
loading: createLoadingSnapshot,
31+
success: createSuccessSnapshot,
32+
error: createErrorSnapshot,
33+
},
34+
} = params;
35+
36+
this.updateSnapshot(updateSnapshot.loading(key));
37+
try {
38+
const data = await service(serviceParams as TParams);
39+
this.updateSnapshot(updateSnapshot.success(key, data));
40+
} catch (error) {
41+
this.updateSnapshot(updateSnapshot.error(key, error));
42+
}
43+
}
44+
}

0 commit comments

Comments
 (0)