From f8eebd5d25b6b579d38b379164be02eb8f85e023 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 25 Oct 2023 16:32:27 -0400 Subject: [PATCH 1/3] Add fetcher data layer --- .changeset/fetcher-data.md | 5 + packages/react-router-dom/index.tsx | 144 ++++++++++++++++++++------- packages/react-router-dom/server.tsx | 31 ++++-- 3 files changed, 135 insertions(+), 45 deletions(-) create mode 100644 .changeset/fetcher-data.md diff --git a/.changeset/fetcher-data.md b/.changeset/fetcher-data.md new file mode 100644 index 0000000000..7595f1404f --- /dev/null +++ b/.changeset/fetcher-data.md @@ -0,0 +1,5 @@ +--- +"react-router-dom": patch +--- + +Adds a fetcher context to `RouterProvider` that holds completed fetcher data, in preparation for the upcoming future flag that will change the fetcher persistence/cleanup behavior diff --git a/packages/react-router-dom/index.tsx b/packages/react-router-dom/index.tsx index f9aa56e0f4..817ed1d8b1 100644 --- a/packages/react-router-dom/index.tsx +++ b/packages/react-router-dom/index.tsx @@ -349,6 +349,20 @@ if (__DEV__) { export { ViewTransitionContext as UNSAFE_ViewTransitionContext }; +// TODO: (v7) Change the useFetcher data from `any` to `unknown` +type FetchersContextObject = { + fetcherData: Map; + register: (key: string) => void; + unregister: (key: string) => void; +}; + +const FetchersContext = React.createContext(null); +if (__DEV__) { + FetchersContext.displayName = "Fetchers"; +} + +export { FetchersContext as UNSAFE_FetchersContext }; + //#endregion //////////////////////////////////////////////////////////////////////////////// @@ -427,6 +441,7 @@ export function RouterProvider({ router, future, }: RouterProviderProps): React.ReactElement { + let { fetcherContext, fetcherData } = useFetcherDataLayer(); let [state, setStateImpl] = React.useState(router.state); let [pendingState, setPendingState] = React.useState(); let [vtContext, setVtContext] = React.useState({ @@ -457,6 +472,12 @@ export function RouterProvider({ newState: RouterState, { unstable_viewTransitionOpts: viewTransitionOpts } ) => { + newState.fetchers.forEach((fetcher, key) => { + if (fetcher.data !== undefined) { + fetcherData.current.set(key, fetcher.data); + } + }); + if ( !viewTransitionOpts || router.window == null || @@ -484,7 +505,7 @@ export function RouterProvider({ }); } }, - [optInStartTransition, transition, renderDfd, router.window] + [router.window, transition, renderDfd, fetcherData, optInStartTransition] ); // Need to use a layout effect here so we are subscribed early enough to @@ -587,20 +608,22 @@ export function RouterProvider({ <> - - - {state.initialized ? ( - - ) : ( - fallbackElement - )} - - + + + + {state.initialized ? ( + + ) : ( + fallbackElement + )} + + + {null} @@ -1198,6 +1221,8 @@ enum DataRouterStateHook { UseScrollRestoration = "useScrollRestoration", } +// Internal hooks + function getDataRouterConsoleError( hookName: DataRouterHook | DataRouterStateHook ) { @@ -1216,6 +1241,49 @@ function useDataRouterState(hookName: DataRouterStateHook) { return state; } +function useFetcherDataLayer() { + let fetcherRefs = React.useRef>(new Map()); + let fetcherData = React.useRef>(new Map()); + + let registerFetcher = React.useCallback( + (key: string) => { + let count = fetcherRefs.current.get(key); + if (count == null) { + fetcherRefs.current.set(key, 1); + } else { + fetcherRefs.current.set(key, count + 1); + } + }, + [fetcherRefs] + ); + + let unregisterFetcher = React.useCallback( + (key: string) => { + let count = fetcherRefs.current.get(key); + if (count == null || count <= 1) { + fetcherRefs.current.delete(key); + fetcherData.current.delete(key); + } else { + fetcherRefs.current.set(key, count - 1); + } + }, + [fetcherData, fetcherRefs] + ); + + let fetcherContext = React.useMemo( + () => ({ + fetcherData: fetcherData.current, + register: registerFetcher, + unregister: unregisterFetcher, + }), + [fetcherData, registerFetcher, unregisterFetcher] + ); + + return { fetcherContext, fetcherData }; +} + +// External hooks + /** * Handles the click behavior for router `` components. This is useful if * you need to create custom `` components with the same click behavior we @@ -1499,20 +1567,41 @@ export function useFetcher({ key, }: { key?: string } = {}): FetcherWithComponents { let { router } = useDataRouterContext(DataRouterHook.UseFetcher); + let fetchersContext = React.useContext(FetchersContext); let route = React.useContext(RouteContext); - invariant(route, `useFetcher must be used inside a RouteContext`); - let routeId = route.matches[route.matches.length - 1]?.route.id; + + invariant( + fetchersContext, + `useFetcher must be used inside a FetchersContext` + ); + invariant(route, `useFetcher must be used inside a RouteContext`); invariant( routeId != null, `useFetcher can only be used on routes that contain a unique "id"` ); + // Fetcher key handling let [fetcherKey, setFetcherKey] = React.useState(key || ""); if (!fetcherKey) { setFetcherKey(getUniqueFetcherId()); } + // Registration/cleanup + let { fetcherData, register, unregister } = fetchersContext; + React.useEffect(() => { + register(fetcherKey); + return () => { + unregister(fetcherKey); + if (!router) { + console.warn(`No router available to clean up from useFetcher()`); + return; + } + router.deleteFetcher(fetcherKey); + }; + }, [router, fetcherKey, register, unregister]); + + // Fetcher additions let load = React.useCallback( (href: string) => { invariant(router, "No router available for fetcher.load()"); @@ -1521,8 +1610,6 @@ export function useFetcher({ }, [fetcherKey, routeId, router] ); - - // Fetcher additions (submit) let submitImpl = useSubmit(); let submit = React.useCallback( (target, opts) => { @@ -1548,31 +1635,20 @@ export function useFetcher({ return FetcherForm; }, [fetcherKey]); + // Exposed FetcherWithComponents let fetcher = router.getFetcher(fetcherKey); - + let data = fetcherData.get(fetcherKey); let fetcherWithComponents = React.useMemo( () => ({ Form: FetcherForm, submit, load, ...fetcher, + data, }), - [fetcher, FetcherForm, submit, load] + [FetcherForm, submit, load, fetcher, data] ); - React.useEffect(() => { - // Is this busted when the React team gets real weird and calls effects - // twice on mount? We really just need to garbage collect here when this - // fetcher is no longer around. - return () => { - if (!router) { - console.warn(`No router available to clean up from useFetcher()`); - return; - } - router.deleteFetcher(fetcherKey); - }; - }, [router, fetcherKey]); - return fetcherWithComponents; } diff --git a/packages/react-router-dom/server.tsx b/packages/react-router-dom/server.tsx index 7946218731..b8fd113bd2 100644 --- a/packages/react-router-dom/server.tsx +++ b/packages/react-router-dom/server.tsx @@ -34,6 +34,7 @@ import { Router, UNSAFE_DataRouterContext as DataRouterContext, UNSAFE_DataRouterStateContext as DataRouterStateContext, + UNSAFE_FetchersContext as FetchersContext, UNSAFE_ViewTransitionContext as ViewTransitionContext, } from "react-router-dom"; @@ -132,17 +133,25 @@ export function StaticRouterProvider({ <> - - - - - + (), + register: () => {}, + unregister: () => {}, + }} + > + + + + + + {hydrateScript ? ( From e1215e9a1ec0bab8148694ecb81036810ea41252 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 25 Oct 2023 16:54:31 -0400 Subject: [PATCH 2/3] Bump bundle --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 9a3add99b2..d59dbca080 100644 --- a/package.json +++ b/package.json @@ -116,13 +116,13 @@ "none": "13.9 kB" }, "packages/react-router/dist/umd/react-router.production.min.js": { - "none": "16.3 kB" + "none": "16.5 kB" }, "packages/react-router-dom/dist/react-router-dom.production.min.js": { "none": "15.9 kB" }, "packages/react-router-dom/dist/umd/react-router-dom.production.min.js": { - "none": "22.1 kB" + "none": "22.7 kB" } } } From a9c1886aafa56752e7cd0ef1f5e46d64250fa7e2 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 25 Oct 2023 18:14:09 -0400 Subject: [PATCH 3/3] Bump bundle --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index d59dbca080..8e48624414 100644 --- a/package.json +++ b/package.json @@ -116,10 +116,10 @@ "none": "13.9 kB" }, "packages/react-router/dist/umd/react-router.production.min.js": { - "none": "16.5 kB" + "none": "16.3 kB" }, "packages/react-router-dom/dist/react-router-dom.production.min.js": { - "none": "15.9 kB" + "none": "16.5 kB" }, "packages/react-router-dom/dist/umd/react-router-dom.production.min.js": { "none": "22.7 kB"