UNPKG

@finos/legend-application

Version:
140 lines (128 loc) 5.07 kB
/** * Copyright (c) 2020-present, Goldman Sachs * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { useEffect, useRef } from 'react'; import type { SetURLSearchParams } from 'react-router'; /** * Module-level queue for pending search param updates. * This ensures all instances of the hook share the same queue, * allowing updates to be batched together and applied in a single * setSearchParams call. * * This is necessary because react-router's setSearchParams does not * support queueing like React's setState does. Multiple calls to * setSearchParams in the same tick will not build on the prior value. * See: https://github.com/remix-run/react-router/issues/9304 */ const pendingUpdates: Map<string, string | null> = new Map(); let flushScheduled = false; let currentSetSearchParams: SetURLSearchParams | null = null; const flushUpdates = (): void => { if (pendingUpdates.size === 0 || !currentSetSearchParams) { flushScheduled = false; return; } const setSearchParams = currentSetSearchParams; const updates = new Map(pendingUpdates); pendingUpdates.clear(); flushScheduled = false; setSearchParams((params) => { const newParams = new URLSearchParams(params); updates.forEach((value, key) => { if (value !== null) { newParams.set(key, value); } else { newParams.delete(key); } }); return newParams; }); }; const queueUpdate = ( key: string, value: string | null, setSearchParams: SetURLSearchParams, ): void => { pendingUpdates.set(key, value); currentSetSearchParams = setSearchParams; if (!flushScheduled) { flushScheduled = true; // Use queueMicrotask to batch updates in the same tick queueMicrotask(flushUpdates); } }; /** * Util hook to keep a state variable in sync with a URL search parameter. * This hook syncs from URL to state if the state is null/undefined (so * initial value is set from URL) and from state to URL otherwise. * * This hook properly queues setSearchParams calls to ensure all updates * are applied, working around react-router's limitation where multiple * calls to setSearchParams in the same tick don't build on each other. * See: https://github.com/remix-run/react-router/issues/9304 * * @param stateVar the state variable to sync * @param updateStateVar setter function to update the state variable (should be memoized with useCallback) * @param searchParamKey the key of the URL search parameter to sync with * @param searchParamValue the current URL search parameter value (i.e., from useSearchParams) * @param setSearchParams function to update the URL search parameters (i.e., from useSearchParams) * @param initializedCallback function to check if the underlying state is initialized and ready to sync with URL (should be memoized with useCallback) */ export const useSyncStateAndSearchParam = ( stateVar: string | boolean | number | null | undefined, updateStateVar: (val: string | null) => void, searchParamKey: string, searchParamValue: string | null, setSearchParams: SetURLSearchParams, initializedCallback: () => boolean, clearParamOnUnmount: boolean = true, ): void => { // Use a ref to make setSearchParams stable // react-router's setSearchParams is not stable and changes on every render // See: https://github.com/remix-run/react-router/issues/9304 const setSearchParamsRef = useRef(setSearchParams); setSearchParamsRef.current = setSearchParams; // Sync state with URL search param if state is null/undefined useEffect(() => { if ( initializedCallback() && (stateVar === null || stateVar === undefined) ) { updateStateVar(searchParamValue); } }, [initializedCallback, searchParamValue, stateVar, updateStateVar]); // Sync URL search param with state useEffect(() => { if (initializedCallback()) { // When state changes, queue URL param update // Using the queueing mechanism ensures all updates are applied // even when multiple hooks call setSearchParams in the same tick if (stateVar !== null && stateVar !== undefined) { queueUpdate( searchParamKey, String(stateVar), setSearchParamsRef.current, ); } else { queueUpdate(searchParamKey, null, setSearchParamsRef.current); } } return () => { if (clearParamOnUnmount) { queueUpdate(searchParamKey, null, setSearchParamsRef.current); } }; }, [clearParamOnUnmount, initializedCallback, searchParamKey, stateVar]); };