/
useData.ts
153 lines (137 loc) · 4.68 KB
/
useData.ts
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
import {Reducer, useCallback, useEffect, useMemo, useReducer, useRef} from 'react';
/* --- Types --- */
export interface UseDataOptions {
/**
* Should the hook fire the `asyncFetch` on mount.
* @default true
*/
fireOnMount?: boolean;
/**
* Should the hook take every call rather than throwing out active calls when
* new ones are made.
* @default false
*/
takeEvery?: boolean;
}
type UseDataAction<D> =
| {type: 'FETCH_INIT'}
| {type: 'FETCH_SUCCESS'; payload: D}
| {type: 'FETCH_FAILURE'; payload: Error}
| {type: 'CHANGE_DATA'; payload: {data: D; stopLoading: boolean}};
interface UseDataState<D> {
/** True if currently fetching. */
loading: boolean;
/** The error object if your fetch fails. */
error: Error | null;
/** The data from your async fetch, or null if not fetched. */
data: D | null;
}
export interface StatusObject<D> extends UseDataState<D> {
/**
* Fire the async function that was provided to useData.
* @param newAsyncFetch Optional. If provided, will call this instead of `asyncFetch`.
*/
fireFetch: (newAsyncFetch?: () => Promise<D>) => void;
/**
* Mutate the data.
* @param newData A new data, or a function that is given old data and returns the new data.
* @param stopLoading Optional. Default false. Should loading stop when setData is called?
*/
setData: (newData: D | ((oldData: D | null) => D), stopLoading?: boolean) => void;
}
/* --- Functions --- */
/** Generates the reducer in order to allow for generic typing. */
const dataFetchReducer = <D>() => {
const reducer: Reducer<UseDataState<D>, UseDataAction<D>> = (state, action) => {
switch (action.type) {
case 'FETCH_INIT':
if (state.loading === true) return state;
return {...state, loading: true};
case 'FETCH_SUCCESS':
return {...state, loading: false, error: null, data: action.payload};
case 'FETCH_FAILURE':
return {...state, loading: false, error: action.payload};
case 'CHANGE_DATA':
return {
...state,
loading: action.payload.stopLoading ? false : state.loading,
error: null,
data: action.payload.data,
};
default:
throw new Error(`Invalid reducer type`);
}
};
return reducer;
};
/**
* Makes an async call to the provided function on component mount, and provides
* an array containing an `IStatusObject` that displays loading status, fetched
* data, and errors, and a callback function to retry the `asyncFetch`.
*
* @param asyncFetch The async function that fetches your data.
* @param initialData Optional. If given will initially populate data.
* @param options Optional. Additional options for the hook.
*/
export function useData<D>(
asyncFetch: () => Promise<D>,
initialData?: D,
options: UseDataOptions = {},
): StatusObject<D> {
const fireOnMount = options.fireOnMount === undefined ? true : options.fireOnMount;
const takeEvery = options.takeEvery === undefined ? false : options.takeEvery;
const fnStartTime = useRef<number | null>(null);
// https://overreacted.io/making-setinterval-declarative-with-react-hooks/#refs-to-the-rescue
const savedAsyncFunc = useRef(asyncFetch);
useEffect(() => {
savedAsyncFunc.current = asyncFetch;
});
const [state, dispatch] = useReducer(dataFetchReducer<D>(), {
loading: fireOnMount,
error: null,
data: initialData || null,
});
const fetchData = useCallback<(newAsyncFetch?: typeof asyncFetch) => void>(
async newAsyncFetch => {
const startTime = new Date().getTime();
fnStartTime.current = startTime;
dispatch({type: 'FETCH_INIT'});
try {
let data;
if (newAsyncFetch) {
data = await newAsyncFetch();
} else {
data = await savedAsyncFunc.current();
}
if (takeEvery || fnStartTime.current === startTime) {
dispatch({type: 'FETCH_SUCCESS', payload: data});
}
} catch (error) {
dispatch({type: 'FETCH_FAILURE', payload: error});
}
},
[takeEvery],
);
useEffect(() => {
fireOnMount && fetchData();
}, [fireOnMount, fetchData]);
const fireFetch = useCallback<StatusObject<D>['fireFetch']>(
newAsyncFetch => {
fetchData(newAsyncFetch);
},
[fetchData],
);
const setData = useCallback<StatusObject<D>['setData']>(
(newData, stopLoading = false) => {
let data;
if (typeof newData === 'function') {
data = (newData as Function)(state.data);
} else {
data = newData;
}
dispatch({type: 'CHANGE_DATA', payload: {data, stopLoading}});
},
[state.data],
);
return useMemo<StatusObject<D>>(() => ({...state, fireFetch, setData}), [state, fireFetch, setData]);
}