Skip to content

Commit

Permalink
Fix re-renders in globalApiState
Browse files Browse the repository at this point in the history
See comment for explanation of the new design,
we were seeing multiple fetches.
  • Loading branch information
rgalanakis committed Aug 30, 2024
1 parent 1da3b06 commit ca450ac
Showing 1 changed file with 39 additions and 11 deletions.
50 changes: 39 additions & 11 deletions adminapp/src/hooks/globalApiState.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<GlobalApiStateContext.Provider value={{ state, setState }}>
<GlobalApiStateContext.Provider value={{ hasKey, getKey, setKey }}>
{children}
</GlobalApiStateContext.Provider>
);
}

function pickData(r) {
return r.data;
}
const defaultOptions = { pick: pickData };

0 comments on commit ca450ac

Please sign in to comment.