diff --git a/adminapp/src/hooks/globalApiState.jsx b/adminapp/src/hooks/globalApiState.jsx index 8f01cb42..88b2611d 100644 --- a/adminapp/src/hooks/globalApiState.jsx +++ b/adminapp/src/hooks/globalApiState.jsx @@ -15,34 +15,62 @@ const loading = Symbol("globalApiStateLoading"); * @param {object=} options * @param {function=} options.pick Called with the response, to pick the data for the state. * Defaults to `(r) => r.data`. - * @returns {*} + * @returns {*} The default value before the API call is resolved + * (before it's made, while it's pending, if it's rejected), + * or the picked value (like response.data) once the API call is resolved. */ export function useGlobalApiState(apiGet, defaultValue, options) { - options = merge({ pick: (r) => r.data }, options); - const { state, setState } = React.useContext(GlobalApiStateContext); + options = merge({}, defaultOptions, options); + const { hasKey, getKey, setKey } = React.useContext(GlobalApiStateContext); const { enqueueErrorSnackbar } = useErrorSnackbar(); const key = "" + apiGet; React.useEffect(() => { - if (has(state, key)) { + if (hasKey(key)) { return; } apiGet() - .then((r) => setState({ ...state, [key]: r })) + .then((r) => setKey(key, r)) .catch(enqueueErrorSnackbar); - setState({ ...state, [key]: loading }); - }, [enqueueErrorSnackbar, state, apiGet, setState, key, options]); - if (has(state, key)) { - const r = state[key]; + setKey(key, loading); + }, [enqueueErrorSnackbar, apiGet, key, hasKey, setKey]); + if (hasKey(key)) { + const r = getKey(key); return r === loading ? defaultValue : options.pick(r); } return defaultValue; } export function GlobalApiStateProvider({ children }) { - const [state, setState] = React.useState({}); + // Under the hood, we need to save the state into a ref, + // because we don't want to make multiple API calls for the same request. + // But it is very easy for children to be working with different views of the state + // (ie, when useGlobalApiState is used in multiple places in the tree) + // and trigger multiple api calls (because both components are adding keys when they render). + // + // Whenever we write to the ref, we also modify the state, + // which causes children to re-render, calling getKey and seeing the new state stored in the ref. + // + // If we just had the ref (no state), modifying the ref would not cause any re-renders + // and children would not see the new data. + const storage = React.useRef({}); + const [dummyState, setDummyState] = React.useState({}); + const hasKey = React.useCallback((k) => has(storage.current, k), [storage]); + const getKey = React.useCallback((k) => storage.current[k], [storage]); + const setKey = React.useCallback( + (k, v) => { + storage.current = { ...storage.current, [k]: v }; + setDummyState({ ...dummyState, [k]: v }); + }, + [dummyState] + ); return ( - + {children} ); } + +function pickData(r) { + return r.data; +} +const defaultOptions = { pick: pickData };