floppy-disk 1.0.0
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/LICENSE +21 -0
- package/README.md +612 -0
- package/esm/index.d.ts +6 -0
- package/esm/index.js +6 -0
- package/esm/react/create-mutation.d.ts +21 -0
- package/esm/react/create-mutation.js +48 -0
- package/esm/react/create-query.d.ts +150 -0
- package/esm/react/create-query.js +251 -0
- package/esm/react/create-store.d.ts +22 -0
- package/esm/react/create-store.js +25 -0
- package/esm/react/create-stores.d.ts +34 -0
- package/esm/react/create-stores.js +83 -0
- package/esm/react/with-context.d.ts +5 -0
- package/esm/react/with-context.js +14 -0
- package/esm/utils/index.d.ts +2 -0
- package/esm/utils/index.js +2 -0
- package/esm/vanilla.d.ts +23 -0
- package/esm/vanilla.js +57 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.js +9 -0
- package/lib/react/create-mutation.d.ts +21 -0
- package/lib/react/create-mutation.js +52 -0
- package/lib/react/create-query.d.ts +150 -0
- package/lib/react/create-query.js +255 -0
- package/lib/react/create-store.d.ts +22 -0
- package/lib/react/create-store.js +29 -0
- package/lib/react/create-stores.d.ts +34 -0
- package/lib/react/create-stores.js +87 -0
- package/lib/react/with-context.d.ts +5 -0
- package/lib/react/with-context.js +19 -0
- package/lib/utils/index.d.ts +2 -0
- package/lib/utils/index.js +7 -0
- package/lib/vanilla.d.ts +23 -0
- package/lib/vanilla.js +61 -0
- package/package.json +66 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { noop } from '../utils';
|
|
2
|
+
import { createStore } from './create-store';
|
|
3
|
+
export const createMutation = (mutationFn, options = {}) => {
|
|
4
|
+
const { onMutate = noop, onSuccess = noop, onError = noop, onSettled = noop, ...createStoreOptions } = options;
|
|
5
|
+
const useMutation = createStore(({ set, get }) => ({
|
|
6
|
+
isWaiting: false,
|
|
7
|
+
isSuccess: false,
|
|
8
|
+
isError: false,
|
|
9
|
+
response: null,
|
|
10
|
+
responseUpdatedAt: null,
|
|
11
|
+
error: null,
|
|
12
|
+
errorUpdatedAt: null,
|
|
13
|
+
mutate: (variables) => {
|
|
14
|
+
set({ isWaiting: true });
|
|
15
|
+
const state = get();
|
|
16
|
+
onMutate(variables, state);
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
mutationFn(variables, state)
|
|
19
|
+
.then((response) => {
|
|
20
|
+
set({
|
|
21
|
+
isWaiting: false,
|
|
22
|
+
isSuccess: true,
|
|
23
|
+
response,
|
|
24
|
+
responseUpdatedAt: Date.now(),
|
|
25
|
+
error: null,
|
|
26
|
+
errorUpdatedAt: null,
|
|
27
|
+
});
|
|
28
|
+
onSuccess(response, variables, state);
|
|
29
|
+
resolve(response);
|
|
30
|
+
})
|
|
31
|
+
.catch((error) => {
|
|
32
|
+
set({
|
|
33
|
+
isWaiting: true,
|
|
34
|
+
isError: true,
|
|
35
|
+
error,
|
|
36
|
+
errorUpdatedAt: Date.now(),
|
|
37
|
+
});
|
|
38
|
+
onError(error, variables, state);
|
|
39
|
+
reject(error);
|
|
40
|
+
})
|
|
41
|
+
.finally(() => {
|
|
42
|
+
onSettled(variables, state);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
}), createStoreOptions);
|
|
47
|
+
return useMutation;
|
|
48
|
+
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { CreateStoresOptions, StoreKey } from './create-stores';
|
|
2
|
+
export declare type QueryStatus = 'loading' | 'success' | 'error';
|
|
3
|
+
export declare type QueryState<TKey extends StoreKey = StoreKey, TResponse = any, TData = TResponse, TError = unknown> = {
|
|
4
|
+
/**
|
|
5
|
+
* Query store key, an object that will be hashed into a string as a query store identifier.
|
|
6
|
+
*/
|
|
7
|
+
key: TKey;
|
|
8
|
+
/**
|
|
9
|
+
* Will only be called if the data is stale or empty.
|
|
10
|
+
*/
|
|
11
|
+
fetch: () => void;
|
|
12
|
+
/**
|
|
13
|
+
* Will be called even if the data is still fresh (not stale).
|
|
14
|
+
*/
|
|
15
|
+
forceFetch: () => void;
|
|
16
|
+
/**
|
|
17
|
+
* Fetch next page if has next page.
|
|
18
|
+
*
|
|
19
|
+
* If the data is empty, it will just fetch the first page.
|
|
20
|
+
*
|
|
21
|
+
* You can ignore this if your query is not paginated.
|
|
22
|
+
*/
|
|
23
|
+
fetchNextPage: () => void;
|
|
24
|
+
markAsStale: () => void;
|
|
25
|
+
reset: () => void;
|
|
26
|
+
helpers: {
|
|
27
|
+
/**
|
|
28
|
+
* Fetch all active queries.
|
|
29
|
+
*
|
|
30
|
+
* Same as `fetch` method, this will only be called if the data is stale or empty.
|
|
31
|
+
*/
|
|
32
|
+
fetchAllActiveQueries: () => void;
|
|
33
|
+
/**
|
|
34
|
+
* Fetch all active queries.
|
|
35
|
+
*
|
|
36
|
+
* Same as `fetch` method, this will only be called if the data is stale or empty.
|
|
37
|
+
*/
|
|
38
|
+
forceFetchAllActiveQueries: () => void;
|
|
39
|
+
/**
|
|
40
|
+
* Delete query data for all query keys.
|
|
41
|
+
*/
|
|
42
|
+
resetAllQueries: () => void;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Network fetching status.
|
|
46
|
+
*/
|
|
47
|
+
isWaiting: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Network fetching status for fetching next page.
|
|
50
|
+
*/
|
|
51
|
+
isWaitingNextPage: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Status of the data.
|
|
54
|
+
*
|
|
55
|
+
* `"loading"` = no data.
|
|
56
|
+
*
|
|
57
|
+
* `"success"` = has data.
|
|
58
|
+
*
|
|
59
|
+
* `"error"` = has error.
|
|
60
|
+
*
|
|
61
|
+
* It has no relation with network fetching state.
|
|
62
|
+
* If you're looking for network fetching state, use `isWaiting` instead.
|
|
63
|
+
*/
|
|
64
|
+
status: QueryStatus;
|
|
65
|
+
/**
|
|
66
|
+
* Data state, will be `true` if the query has no data.
|
|
67
|
+
*
|
|
68
|
+
* It has no relation with network fetching state.
|
|
69
|
+
* If you're looking for network fetching state, use `isWaiting` instead.
|
|
70
|
+
*/
|
|
71
|
+
isLoading: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Data state, will be `true` if the query has a data.
|
|
74
|
+
*/
|
|
75
|
+
isSuccess: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Error state, will be `true` after data fetching error.
|
|
78
|
+
*/
|
|
79
|
+
isError: boolean;
|
|
80
|
+
isRefetching: boolean;
|
|
81
|
+
isRefetchError: boolean;
|
|
82
|
+
isPreviousData: boolean;
|
|
83
|
+
data: TData | null;
|
|
84
|
+
response: TResponse | null;
|
|
85
|
+
responseUpdatedAt: number | null;
|
|
86
|
+
error: TError | null;
|
|
87
|
+
errorUpdatedAt: number | null;
|
|
88
|
+
retryCount: number;
|
|
89
|
+
isGoingToRetry: boolean;
|
|
90
|
+
pageParam: any;
|
|
91
|
+
pageParams: any[];
|
|
92
|
+
hasNextPage: boolean;
|
|
93
|
+
};
|
|
94
|
+
export declare type CreateQueryOptions<TKey extends StoreKey = StoreKey, TResponse = any, TData = TResponse, TError = unknown> = CreateStoresOptions<TKey, QueryState<TKey, TResponse, TData, TError>> & {
|
|
95
|
+
select?: (response: TResponse, state: Pick<QueryState<TKey, TResponse, TData, TError>, 'data' | 'key'>) => TData;
|
|
96
|
+
/**
|
|
97
|
+
* Stale time in miliseconds.
|
|
98
|
+
*
|
|
99
|
+
* Defaults to `3000` (3 seconds).
|
|
100
|
+
*/
|
|
101
|
+
staleTime?: number;
|
|
102
|
+
/**
|
|
103
|
+
* Number of maximum retries.
|
|
104
|
+
*
|
|
105
|
+
* Defaults to `1`.
|
|
106
|
+
*/
|
|
107
|
+
retry?: number;
|
|
108
|
+
/**
|
|
109
|
+
* Auto call the query when the component is mounted.
|
|
110
|
+
*
|
|
111
|
+
* Defaults to `true`.
|
|
112
|
+
*
|
|
113
|
+
* - If set to `true`, the query will be called on mount focus **if the data is stale**.
|
|
114
|
+
* - If set to `false`, the query won't be called on mount focus.
|
|
115
|
+
* - If set to `"always"`, the query will be called on mount focus.
|
|
116
|
+
*/
|
|
117
|
+
fetchOnMount?: boolean | 'always' | ((key: TKey) => boolean | 'always');
|
|
118
|
+
/**
|
|
119
|
+
* Defaults to `true`.
|
|
120
|
+
*
|
|
121
|
+
* - If set to `true`, the query will be called on window focus **if the data is stale**.
|
|
122
|
+
* - If set to `false`, the query won't be called on window focus.
|
|
123
|
+
* - If set to `"always"`, the query will be called on window focus.
|
|
124
|
+
*/
|
|
125
|
+
fetchOnWindowFocus?: boolean | 'always' | ((key: TKey) => boolean | 'always');
|
|
126
|
+
/**
|
|
127
|
+
* If set to `false` or return `false`, the query won't be called in any condition.
|
|
128
|
+
* Auto fetch on mount will be disabled.
|
|
129
|
+
* Manually trigger `fetch` method (returned from `createQuery`) won't work too.
|
|
130
|
+
*
|
|
131
|
+
* Defaults to `true`.
|
|
132
|
+
*/
|
|
133
|
+
enabled?: boolean | ((key: TKey) => boolean);
|
|
134
|
+
/**
|
|
135
|
+
* If set to `true`, previous `data` will be kept when fetching new data because the query key changed.
|
|
136
|
+
*
|
|
137
|
+
* This will only happened if there is no `data` in the next query.
|
|
138
|
+
*/
|
|
139
|
+
keepPreviousData?: boolean;
|
|
140
|
+
/**
|
|
141
|
+
* Only set this if you have an infinite query.
|
|
142
|
+
*
|
|
143
|
+
* This function should return a variable that will be used when fetching next page (`pageParam`).
|
|
144
|
+
*/
|
|
145
|
+
getNextPageParam?: (lastPage: TResponse, index: number) => any;
|
|
146
|
+
onSuccess?: (response: TResponse, inputState: QueryState<TKey, TResponse, TData, TError>) => void;
|
|
147
|
+
onError?: (error: TError, inputState: QueryState<TKey, TResponse, TData, TError>) => void;
|
|
148
|
+
onSettled?: (inputState: QueryState<TKey, TResponse, TData, TError>) => void;
|
|
149
|
+
};
|
|
150
|
+
export declare const createQuery: <TKey extends StoreKey = StoreKey, TResponse = any, TData = TResponse, TError = unknown>(queryFn: (key: TKey, state: QueryState<TKey, TResponse, TData, TError>) => Promise<TResponse>, options?: CreateQueryOptions<TKey, TResponse, TData, TError>) => import("./create-stores").UseStores<TKey, QueryState<TKey, TResponse, TData, TError>>;
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { identityFn, noop } from '../utils';
|
|
2
|
+
import { createStores } from './create-stores';
|
|
3
|
+
const getDecision = (value, param, { ifTrue, ifAlways }) => {
|
|
4
|
+
if (value === true || (typeof value === 'function' && value(param) === true)) {
|
|
5
|
+
ifTrue();
|
|
6
|
+
}
|
|
7
|
+
else if (value === 'always' || (typeof value === 'function' && value(param) === 'always')) {
|
|
8
|
+
ifAlways();
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
const DEFAULT_STALE_TIME = 3000; // 3 seconds
|
|
12
|
+
const INITIAL_QUERY_STATE = {
|
|
13
|
+
isWaiting: false,
|
|
14
|
+
isWaitingNextPage: false,
|
|
15
|
+
status: 'loading',
|
|
16
|
+
isLoading: true,
|
|
17
|
+
isSuccess: false,
|
|
18
|
+
isError: false,
|
|
19
|
+
isRefetching: false,
|
|
20
|
+
isRefetchError: false,
|
|
21
|
+
isPreviousData: false,
|
|
22
|
+
data: null,
|
|
23
|
+
response: null,
|
|
24
|
+
responseUpdatedAt: null,
|
|
25
|
+
error: null,
|
|
26
|
+
errorUpdatedAt: null,
|
|
27
|
+
retryCount: 0,
|
|
28
|
+
isGoingToRetry: false,
|
|
29
|
+
pageParam: undefined,
|
|
30
|
+
pageParams: [undefined],
|
|
31
|
+
hasNextPage: false,
|
|
32
|
+
};
|
|
33
|
+
const useQueryDefaultDeps = (state) => [
|
|
34
|
+
state.data,
|
|
35
|
+
state.error,
|
|
36
|
+
state.isWaitingNextPage,
|
|
37
|
+
state.hasNextPage,
|
|
38
|
+
];
|
|
39
|
+
export const createQuery = (queryFn, options = {}) => {
|
|
40
|
+
const { onFirstSubscribe = noop, onSubscribe = noop, onLastUnsubscribe = noop, onBeforeChangeKey = noop, defaultDeps = useQueryDefaultDeps, select = identityFn, staleTime = DEFAULT_STALE_TIME, retry = 1, fetchOnMount = true, fetchOnWindowFocus = true, enabled = true, keepPreviousData, getNextPageParam = () => undefined, onSuccess = noop, onError = noop, onSettled = noop, ...createStoresOptions } = options;
|
|
41
|
+
const useQuery = createStores(({ key: _key, get, set }) => {
|
|
42
|
+
const key = _key;
|
|
43
|
+
const forceFetch = () => {
|
|
44
|
+
const responseAllPages = [];
|
|
45
|
+
const newPageParams = [undefined];
|
|
46
|
+
let pageParam = undefined;
|
|
47
|
+
const { isWaiting, isLoading, isGoingToRetry, pageParams } = get();
|
|
48
|
+
if (isWaiting || enabled === false || (typeof enabled === 'function' && !enabled(key)))
|
|
49
|
+
return;
|
|
50
|
+
if (isLoading)
|
|
51
|
+
set({ isWaiting: true });
|
|
52
|
+
else
|
|
53
|
+
set({ isWaiting: true, isRefetching: true });
|
|
54
|
+
const callQuery = () => {
|
|
55
|
+
if (isGoingToRetry) {
|
|
56
|
+
if (isLoading)
|
|
57
|
+
set({ isGoingToRetry: false, isWaiting: true });
|
|
58
|
+
else
|
|
59
|
+
set({ isGoingToRetry: false, isWaiting: true, isRefetching: true });
|
|
60
|
+
}
|
|
61
|
+
const inputState = { ...get(), pageParam };
|
|
62
|
+
queryFn(key, inputState)
|
|
63
|
+
.then((response) => {
|
|
64
|
+
responseAllPages.push(response);
|
|
65
|
+
const newPageParam = getNextPageParam(response, responseAllPages.length);
|
|
66
|
+
newPageParams.push(newPageParam);
|
|
67
|
+
if (newPageParam !== undefined && newPageParams.length < pageParams.length) {
|
|
68
|
+
pageParam = newPageParam;
|
|
69
|
+
callQuery();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
set({
|
|
73
|
+
isWaiting: false,
|
|
74
|
+
status: 'success',
|
|
75
|
+
isLoading: false,
|
|
76
|
+
isSuccess: true,
|
|
77
|
+
isError: false,
|
|
78
|
+
isRefetching: false,
|
|
79
|
+
isRefetchError: false,
|
|
80
|
+
isPreviousData: false,
|
|
81
|
+
data: responseAllPages.reduce((prev, response) => {
|
|
82
|
+
return select(response, { key, data: prev });
|
|
83
|
+
}, null),
|
|
84
|
+
response,
|
|
85
|
+
responseUpdatedAt: Date.now(),
|
|
86
|
+
error: null,
|
|
87
|
+
errorUpdatedAt: null,
|
|
88
|
+
retryCount: 0,
|
|
89
|
+
pageParam: newPageParam,
|
|
90
|
+
pageParams: newPageParams,
|
|
91
|
+
hasNextPage: newPageParam !== undefined,
|
|
92
|
+
});
|
|
93
|
+
onSuccess(response, inputState);
|
|
94
|
+
})
|
|
95
|
+
.catch((error) => {
|
|
96
|
+
const prevState = get();
|
|
97
|
+
const errorUpdatedAt = Date.now();
|
|
98
|
+
set(prevState.isSuccess
|
|
99
|
+
? {
|
|
100
|
+
isWaiting: false,
|
|
101
|
+
isRefetching: false,
|
|
102
|
+
isRefetchError: true,
|
|
103
|
+
data: responseAllPages.reduce((prev, response) => {
|
|
104
|
+
return select(response, { key, data: prev });
|
|
105
|
+
}, null),
|
|
106
|
+
error,
|
|
107
|
+
errorUpdatedAt,
|
|
108
|
+
pageParam,
|
|
109
|
+
hasNextPage: pageParam !== undefined,
|
|
110
|
+
}
|
|
111
|
+
: {
|
|
112
|
+
isWaiting: false,
|
|
113
|
+
isError: true,
|
|
114
|
+
error,
|
|
115
|
+
errorUpdatedAt,
|
|
116
|
+
pageParam,
|
|
117
|
+
hasNextPage: pageParam !== undefined,
|
|
118
|
+
});
|
|
119
|
+
if (prevState.retryCount < retry) {
|
|
120
|
+
set({ retryCount: prevState.retryCount + 1, isGoingToRetry: true });
|
|
121
|
+
callQuery();
|
|
122
|
+
}
|
|
123
|
+
onError(error, inputState);
|
|
124
|
+
})
|
|
125
|
+
.finally(() => {
|
|
126
|
+
onSettled(inputState);
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
callQuery();
|
|
130
|
+
};
|
|
131
|
+
const fetch = () => {
|
|
132
|
+
const { responseUpdatedAt } = get();
|
|
133
|
+
const isStale = Date.now() > Number(responseUpdatedAt) + staleTime;
|
|
134
|
+
if (!isStale)
|
|
135
|
+
return;
|
|
136
|
+
forceFetch();
|
|
137
|
+
};
|
|
138
|
+
const fetchNextPage = () => {
|
|
139
|
+
const state = get();
|
|
140
|
+
const { isLoading, isWaitingNextPage, data, hasNextPage, pageParam, pageParams } = state;
|
|
141
|
+
if (isLoading)
|
|
142
|
+
return forceFetch();
|
|
143
|
+
if (isWaitingNextPage || !hasNextPage)
|
|
144
|
+
return;
|
|
145
|
+
queryFn(key, { ...state, pageParam })
|
|
146
|
+
.then((response) => {
|
|
147
|
+
const newPageParam = getNextPageParam(response, pageParams.length);
|
|
148
|
+
set({
|
|
149
|
+
isWaitingNextPage: false,
|
|
150
|
+
response,
|
|
151
|
+
responseUpdatedAt: Date.now(),
|
|
152
|
+
data: select(response, { key, data }),
|
|
153
|
+
pageParam: newPageParam,
|
|
154
|
+
pageParams: pageParams.concat(newPageParam),
|
|
155
|
+
hasNextPage: newPageParam !== undefined,
|
|
156
|
+
});
|
|
157
|
+
})
|
|
158
|
+
.catch((error) => {
|
|
159
|
+
set({
|
|
160
|
+
isWaitingNextPage: false,
|
|
161
|
+
isError: true,
|
|
162
|
+
error,
|
|
163
|
+
errorUpdatedAt: Date.now(),
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
};
|
|
167
|
+
const fetchAllActiveQueries = () => {
|
|
168
|
+
useQuery.getAllWithSubscriber().forEach((state) => {
|
|
169
|
+
state.fetch();
|
|
170
|
+
});
|
|
171
|
+
};
|
|
172
|
+
const forceFetchAllActiveQueries = () => {
|
|
173
|
+
useQuery.getAllWithSubscriber().forEach((state) => {
|
|
174
|
+
state.forceFetch();
|
|
175
|
+
});
|
|
176
|
+
};
|
|
177
|
+
const resetAllQueries = () => {
|
|
178
|
+
useQuery.getAll().forEach((state) => {
|
|
179
|
+
state.reset();
|
|
180
|
+
});
|
|
181
|
+
};
|
|
182
|
+
return {
|
|
183
|
+
...INITIAL_QUERY_STATE,
|
|
184
|
+
key,
|
|
185
|
+
fetch,
|
|
186
|
+
forceFetch,
|
|
187
|
+
fetchNextPage,
|
|
188
|
+
markAsStale: () => set({ responseUpdatedAt: null }),
|
|
189
|
+
reset: () => set(INITIAL_QUERY_STATE),
|
|
190
|
+
helpers: {
|
|
191
|
+
fetchAllActiveQueries,
|
|
192
|
+
forceFetchAllActiveQueries,
|
|
193
|
+
resetAllQueries,
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}, (() => {
|
|
197
|
+
const resetRetryCount = (key) => {
|
|
198
|
+
useQuery.set(key, { retryCount: 0 }, true);
|
|
199
|
+
};
|
|
200
|
+
const fetchWindowFocusHandler = () => {
|
|
201
|
+
useQuery.getAllWithSubscriber().forEach((state) => {
|
|
202
|
+
getDecision(fetchOnWindowFocus, state.key, {
|
|
203
|
+
ifTrue: state.fetch,
|
|
204
|
+
ifAlways: state.forceFetch,
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
};
|
|
208
|
+
return {
|
|
209
|
+
...createStoresOptions,
|
|
210
|
+
defaultDeps,
|
|
211
|
+
onFirstSubscribe: (state) => {
|
|
212
|
+
if (typeof window !== 'undefined' && fetchOnWindowFocus) {
|
|
213
|
+
window.addEventListener('focus', fetchWindowFocusHandler);
|
|
214
|
+
}
|
|
215
|
+
onFirstSubscribe(state);
|
|
216
|
+
},
|
|
217
|
+
onSubscribe: (state) => {
|
|
218
|
+
getDecision(fetchOnMount, state.key, {
|
|
219
|
+
ifTrue: state.fetch,
|
|
220
|
+
ifAlways: state.forceFetch,
|
|
221
|
+
});
|
|
222
|
+
onSubscribe(state);
|
|
223
|
+
},
|
|
224
|
+
onLastUnsubscribe: (state) => {
|
|
225
|
+
if (typeof window !== 'undefined' && fetchOnWindowFocus) {
|
|
226
|
+
window.addEventListener('focus', fetchWindowFocusHandler);
|
|
227
|
+
}
|
|
228
|
+
resetRetryCount(state.key);
|
|
229
|
+
onLastUnsubscribe(state);
|
|
230
|
+
},
|
|
231
|
+
onBeforeChangeKey: (nextKey, prevKey) => {
|
|
232
|
+
if (keepPreviousData) {
|
|
233
|
+
const nextData = useQuery.get(nextKey);
|
|
234
|
+
if (!nextData.data) {
|
|
235
|
+
const prevData = useQuery.get(prevKey);
|
|
236
|
+
if (prevData.data) {
|
|
237
|
+
useQuery.set(nextKey, {
|
|
238
|
+
data: prevData.data,
|
|
239
|
+
response: prevData.response,
|
|
240
|
+
isLoading: false,
|
|
241
|
+
isPreviousData: true,
|
|
242
|
+
}, true);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
onBeforeChangeKey(nextKey, prevKey);
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
})());
|
|
250
|
+
return useQuery;
|
|
251
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { InitStoreOptions, SelectDeps, SetStoreData, StoreData, StoreInitializer, Subscribers } from '../vanilla';
|
|
2
|
+
export declare type WatchProps<T> = {
|
|
3
|
+
selectDeps?: SelectDeps<T>;
|
|
4
|
+
render: (state: T) => any;
|
|
5
|
+
};
|
|
6
|
+
export declare type UseStore<T extends StoreData> = {
|
|
7
|
+
/**
|
|
8
|
+
* @param selectDeps A function that return the dependency array (just like in `useEffect`), to trigger reactivity.
|
|
9
|
+
* Defaults to `undefined` (reactive to all state change) if you didn't set `defaultDeps` on `createStore`.
|
|
10
|
+
*
|
|
11
|
+
* IMPORTANT NOTE: `selectDeps` must not be changed after initialization.
|
|
12
|
+
*/
|
|
13
|
+
(selectDeps?: SelectDeps<T>): T;
|
|
14
|
+
get: () => T;
|
|
15
|
+
set: (value: SetStoreData<T>, silent?: boolean) => void;
|
|
16
|
+
subscribe: (fn: (state: T) => void, selectDeps?: SelectDeps<T>) => () => void;
|
|
17
|
+
getSubscribers: () => Subscribers<T>;
|
|
18
|
+
Watch: (props: WatchProps<T>) => any;
|
|
19
|
+
};
|
|
20
|
+
export declare const createStore: <T extends StoreData>(initializer: StoreInitializer<T>, options?: InitStoreOptions<T> & {
|
|
21
|
+
defaultDeps?: SelectDeps<T>;
|
|
22
|
+
}) => UseStore<T>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { initStore, } from '../vanilla';
|
|
3
|
+
export const createStore = (initializer, options = {}) => {
|
|
4
|
+
const { get, set, subscribe, getSubscribers } = initStore(initializer, options);
|
|
5
|
+
const { defaultDeps } = options;
|
|
6
|
+
/**
|
|
7
|
+
* IMPORTANT NOTE: selectDeps function must not be changed after initialization.
|
|
8
|
+
*/
|
|
9
|
+
const useStore = (selectDeps = defaultDeps) => {
|
|
10
|
+
const [state, setState] = useState(get);
|
|
11
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
12
|
+
useEffect(() => subscribe(setState, selectDeps), []);
|
|
13
|
+
return state;
|
|
14
|
+
};
|
|
15
|
+
useStore.get = get;
|
|
16
|
+
useStore.set = set;
|
|
17
|
+
useStore.subscribe = subscribe;
|
|
18
|
+
useStore.getSubscribers = getSubscribers;
|
|
19
|
+
const Watch = ({ selectDeps, render }) => {
|
|
20
|
+
const store = useStore(selectDeps);
|
|
21
|
+
return render(store);
|
|
22
|
+
};
|
|
23
|
+
useStore.Watch = Watch;
|
|
24
|
+
return useStore;
|
|
25
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { InitStoreOptions, SelectDeps, SetStoreData, StoreData, Subscribers } from '../vanilla';
|
|
2
|
+
import { WatchProps } from './create-store';
|
|
3
|
+
export declare type StoreKey = Record<string, any> | undefined;
|
|
4
|
+
export declare type StoresInitializer<TKey extends StoreKey = StoreKey, T extends StoreData = StoreData> = (api: {
|
|
5
|
+
key: TKey;
|
|
6
|
+
get: () => T;
|
|
7
|
+
set: (value: SetStoreData<T>, silent?: boolean) => void;
|
|
8
|
+
}) => T;
|
|
9
|
+
export declare type UseStores<TKey extends StoreKey = StoreKey, T extends StoreData = StoreData> = {
|
|
10
|
+
/**
|
|
11
|
+
* @param key (Optional) Store key, an object that will be hashed into a string as a store identifier.
|
|
12
|
+
*
|
|
13
|
+
* @param selectDeps A function that return the dependency array (just like in `useEffect`), to trigger reactivity.
|
|
14
|
+
* Defaults to `undefined` (reactive to all state change) if you didn't set `defaultDeps` on `createStores`.
|
|
15
|
+
*
|
|
16
|
+
* IMPORTANT NOTE: `selectDeps` must not be changed after initialization.
|
|
17
|
+
*/
|
|
18
|
+
(...args: [TKey?, SelectDeps<T>?] | [SelectDeps<T>?]): T;
|
|
19
|
+
get: (key?: TKey) => T;
|
|
20
|
+
getAll: () => T[];
|
|
21
|
+
getAllWithSubscriber: () => T[];
|
|
22
|
+
set: (key: TKey, value: SetStoreData<T>, silent?: boolean) => void;
|
|
23
|
+
setAll: (value: SetStoreData<T>, silent?: boolean) => void;
|
|
24
|
+
subscribe: (key: TKey, fn: (state: T) => void, selectDeps?: SelectDeps<T>) => () => void;
|
|
25
|
+
getSubscribers: (key: TKey) => Subscribers<T>;
|
|
26
|
+
Watch: (props: WatchProps<T> & {
|
|
27
|
+
storeKey?: TKey;
|
|
28
|
+
}) => any;
|
|
29
|
+
};
|
|
30
|
+
export declare type CreateStoresOptions<TKey extends StoreKey = StoreKey, T extends StoreData = StoreData> = InitStoreOptions<T> & {
|
|
31
|
+
onBeforeChangeKey?: (nextKey: TKey, prevKey: TKey) => void;
|
|
32
|
+
defaultDeps?: SelectDeps<T>;
|
|
33
|
+
};
|
|
34
|
+
export declare const createStores: <TKey extends StoreKey = StoreKey, T extends StoreData = StoreData>(initializer: StoresInitializer<TKey, T>, options?: CreateStoresOptions<TKey, T>) => UseStores<TKey, T>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { noop } from '../utils';
|
|
3
|
+
import { initStore, } from '../vanilla';
|
|
4
|
+
const hashStoreKey = (obj) => JSON.stringify(obj, Object.keys(obj).sort());
|
|
5
|
+
export const createStores = (initializer, options = {}) => {
|
|
6
|
+
const { onBeforeChangeKey = noop, defaultDeps } = options;
|
|
7
|
+
const stores = new Map();
|
|
8
|
+
const getStore = (key) => {
|
|
9
|
+
const normalizedKey = hashStoreKey(key);
|
|
10
|
+
if (!stores.has(normalizedKey)) {
|
|
11
|
+
stores.set(normalizedKey, initStore((api) => initializer({ key, ...api }), options));
|
|
12
|
+
}
|
|
13
|
+
return stores.get(normalizedKey);
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* IMPORTANT NOTE: selectDeps function must not be changed after initialization.
|
|
17
|
+
*/
|
|
18
|
+
const useStores = (...args) => {
|
|
19
|
+
const [_key = {}, selectDeps = defaultDeps] = (typeof args[0] === 'function' ? [{}, args[0]] : args);
|
|
20
|
+
const key = _key;
|
|
21
|
+
const normalizedKey = hashStoreKey(key);
|
|
22
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
23
|
+
const { get, subscribe } = useMemo(() => getStore(key), [normalizedKey]);
|
|
24
|
+
const [state, setState] = useState(get);
|
|
25
|
+
const isFirstRender = useRef(true);
|
|
26
|
+
const prevKey = useRef(key);
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (!isFirstRender.current) {
|
|
29
|
+
onBeforeChangeKey(key, prevKey.current);
|
|
30
|
+
setState(get);
|
|
31
|
+
}
|
|
32
|
+
isFirstRender.current = false;
|
|
33
|
+
prevKey.current = key;
|
|
34
|
+
const unsubs = subscribe(setState, selectDeps);
|
|
35
|
+
return unsubs;
|
|
36
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
37
|
+
}, [normalizedKey]);
|
|
38
|
+
return state;
|
|
39
|
+
};
|
|
40
|
+
useStores.get = (key = {}) => {
|
|
41
|
+
const store = getStore(key);
|
|
42
|
+
return store.get();
|
|
43
|
+
};
|
|
44
|
+
useStores.getAll = () => {
|
|
45
|
+
const allStores = [];
|
|
46
|
+
stores.forEach((store) => {
|
|
47
|
+
allStores.push(store.get());
|
|
48
|
+
});
|
|
49
|
+
return allStores;
|
|
50
|
+
};
|
|
51
|
+
useStores.getAllWithSubscriber = () => {
|
|
52
|
+
const allStores = [];
|
|
53
|
+
stores.forEach((store) => {
|
|
54
|
+
const subscribers = store.getSubscribers();
|
|
55
|
+
if (subscribers.size > 0)
|
|
56
|
+
allStores.push(store.get());
|
|
57
|
+
});
|
|
58
|
+
return allStores;
|
|
59
|
+
};
|
|
60
|
+
useStores.set = (key = {}, value, silent) => {
|
|
61
|
+
const store = getStore(key);
|
|
62
|
+
store.set(value, silent);
|
|
63
|
+
};
|
|
64
|
+
useStores.setAll = (value, silent) => {
|
|
65
|
+
stores.forEach((store) => {
|
|
66
|
+
store.set(value, silent);
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
useStores.subscribe = (key = {}, fn, selectDeps) => {
|
|
70
|
+
const store = getStore(key);
|
|
71
|
+
return store.subscribe(fn, selectDeps);
|
|
72
|
+
};
|
|
73
|
+
useStores.getSubscribers = (key = {}) => {
|
|
74
|
+
const store = getStore(key);
|
|
75
|
+
return store.getSubscribers();
|
|
76
|
+
};
|
|
77
|
+
const Watch = ({ storeKey = {}, selectDeps, render }) => {
|
|
78
|
+
const store = useStores(storeKey, selectDeps);
|
|
79
|
+
return render(store);
|
|
80
|
+
};
|
|
81
|
+
useStores.Watch = Watch;
|
|
82
|
+
return useStores;
|
|
83
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React, { createContext, useContext, useState } from 'react';
|
|
2
|
+
export const withContext = (initFn) => {
|
|
3
|
+
const Context = createContext(null);
|
|
4
|
+
const Provider = ({ children, onInitialize, }) => {
|
|
5
|
+
const [value] = useState(() => {
|
|
6
|
+
const store = initFn();
|
|
7
|
+
onInitialize && onInitialize(store);
|
|
8
|
+
return store;
|
|
9
|
+
});
|
|
10
|
+
return React.createElement(Context.Provider, { value: value }, children);
|
|
11
|
+
};
|
|
12
|
+
const useCurrentContext = () => useContext(Context);
|
|
13
|
+
return [Provider, useCurrentContext];
|
|
14
|
+
};
|
package/esm/vanilla.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare type StoreData = Record<string, any>;
|
|
2
|
+
export declare type SetStoreData<T> = Partial<T> | ((prevState: T) => Partial<T>);
|
|
3
|
+
export declare type SelectDeps<T> = ((state: T) => any[]) | undefined;
|
|
4
|
+
export declare type Subscribers<T> = Map<(state: T) => void, SelectDeps<T>>;
|
|
5
|
+
export declare type StoreInitializer<T> = (api: {
|
|
6
|
+
get: () => T;
|
|
7
|
+
set: (value: SetStoreData<T>, silent?: boolean) => void;
|
|
8
|
+
}) => T;
|
|
9
|
+
export declare type StoreEvent<T> = (state: T) => void;
|
|
10
|
+
export declare type InitStoreOptions<T> = {
|
|
11
|
+
intercept?: (nextState: T, prevState: T) => Partial<T>;
|
|
12
|
+
onFirstSubscribe?: StoreEvent<T>;
|
|
13
|
+
onSubscribe?: StoreEvent<T>;
|
|
14
|
+
onUnsubscribe?: StoreEvent<T>;
|
|
15
|
+
onLastUnsubscribe?: StoreEvent<T>;
|
|
16
|
+
};
|
|
17
|
+
export declare type InitStoreReturn<T> = {
|
|
18
|
+
get: () => T;
|
|
19
|
+
set: (value: SetStoreData<T>, silent?: boolean) => void;
|
|
20
|
+
subscribe: (fn: (state: T) => void, selectDeps?: SelectDeps<T>) => () => void;
|
|
21
|
+
getSubscribers: () => Subscribers<T>;
|
|
22
|
+
};
|
|
23
|
+
export declare const initStore: <T extends StoreData>(initializer: StoreInitializer<T>, options?: InitStoreOptions<T>) => InitStoreReturn<T>;
|