-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
165 lines (150 loc) · 4.77 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
import { useMemo, useCallback } from 'react';
import { useReducerWithThunk } from './use-reducer-thunk';
import { withReducerLogs } from './with-reducer-log';
import { withReducerInterceptor } from './with-reducer-interceptor';
import { makeReducer } from './make-reducer';
import { makeApiAction } from './make-api-action';
// status: 'idle' | 'loading' | 'succeeded' | 'failed'
const baseContexeedState = {
status: 'idle',
mutationStatus: 'idle',
receivedAt: null,
error: null,
data: null
};
// Power to the developer: The several hooks accept the `deps` which get passed
// from the parent. This triggers warning with eslint but it is accounted for.
export function useContexeedApi(config, deps = []) {
const {
name,
slicedState,
interceptor,
requests = {},
mutations = {}
} = config;
// Memoize values
const { initialState, reducer, actions } = useMemo(() => {
// If we're using a sliced state, it needs to start empty. Id keys will be
// added at a later stage resulting in something like:
// {
// key1: baseContexeedState,
// key2: baseContexeedState,
// ...
// }
const initialState = slicedState ? {} : baseContexeedState;
const reducer = withReducerLogs(
withReducerInterceptor(
makeReducer({
name,
// When creating the reducer we need the initial state, to be able to
// use the invalidate function.
initialState,
// The base state is used as the source for missing properties. We
// start from the base state and replace what's needed. In this way we
// ensure state consistency.
baseState: baseContexeedState
}),
interceptor
)
);
// Create the actions needed by the thunk (begin and end), and the
// invalidate action to clean the state.
const actions = {
begin: (params) => ({ ...params, type: `begin/${name}` }),
end: (params) => ({
receivedAt: Date.now(),
...params,
type: `end/${name}`
}),
invalidate: (params) => ({ ...params, type: `invalidate/${name}` })
};
return {
initialState,
reducer,
actions
};
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, [...deps]);
// useReducerWithThunk is the same as React's useReducer but with support for
// dispatching functions.
const [state, dispatch] = useReducerWithThunk(reducer, initialState);
// Create the dispatchable request actions from the functions defined in the
// configuration.
const requestActions = useMemo(
() =>
Object.keys(requests).reduce((acc, fnName) => {
const fn = makeApiAction({
type: 'requests',
fn: requests[fnName],
fnName,
config,
actions
});
return {
...acc,
[fnName]: (...args) => dispatch(fn(...args))
};
}, {}),
/* eslint-disable-next-line react-hooks/exhaustive-deps */
[dispatch, ...deps]
);
// Create the dispatchable mutation actions from the functions defined in the
// configuration.
const mutationActions = useMemo(
() =>
Object.keys(mutations).reduce((acc, fnName) => {
const fn = makeApiAction({
type: 'mutations',
fn: mutations[fnName],
fnName,
config,
actions
});
return {
...acc,
[fnName]: (...args) => dispatch(fn(...args))
};
}, {}),
/* eslint-disable-next-line react-hooks/exhaustive-deps */
[dispatch, ...deps]
);
const invalidate = useCallback(
(key) => {
if (slicedState && !key) {
throw new Error(
`The contexeed \`${name}\` is setup as a sliced state (slicedState), but you're using invalidate action without a key value.`
);
}
return dispatch(actions.invalidate(key));
},
/* eslint-disable-next-line react-hooks/exhaustive-deps */
[dispatch, ...deps]
);
const getState = useCallback(
(key) => {
// If the config defines this contexeed as `slicedState`, the `key` becomes
// required.
if (slicedState && !key) {
throw new Error(
`The contexeed \`${name}\` is setup as a sliced state (slicedState), but you're using getState without a key value.`
);
}
if (!slicedState && key) {
throw new Error(
`The contexeed \`${name}\` is not setup as a sliced state (slicedState), but you're using getState with a key value.`
);
}
return (slicedState ? state[key] : state) || baseContexeedState;
},
/* eslint-disable-next-line react-hooks/exhaustive-deps */
[state, ...deps]
);
return {
...requestActions,
...mutationActions,
invalidate,
getState,
rawState: state,
dispatch
};
}