Skip to content

Commit

Permalink
Add fetcher data layer (#10961)
Browse files Browse the repository at this point in the history
  • Loading branch information
brophdawg11 authored Oct 26, 2023
1 parent c0dbcd2 commit cb2d911
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 47 deletions.
5 changes: 5 additions & 0 deletions .changeset/fetcher-data.md
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,10 @@
"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.1 kB"
"none": "22.7 kB"
}
}
}
144 changes: 110 additions & 34 deletions packages/react-router-dom/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any>;
register: (key: string) => void;
unregister: (key: string) => void;
};

const FetchersContext = React.createContext<FetchersContextObject | null>(null);
if (__DEV__) {
FetchersContext.displayName = "Fetchers";
}

export { FetchersContext as UNSAFE_FetchersContext };

//#endregion

////////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -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<RouterState>();
let [vtContext, setVtContext] = React.useState<ViewTransitionContextObject>({
Expand Down Expand Up @@ -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 ||
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -587,20 +608,22 @@ export function RouterProvider({
<>
<DataRouterContext.Provider value={dataRouterContext}>
<DataRouterStateContext.Provider value={state}>
<ViewTransitionContext.Provider value={vtContext}>
<Router
basename={basename}
location={state.location}
navigationType={state.historyAction}
navigator={navigator}
>
{state.initialized ? (
<DataRoutes routes={router.routes} state={state} />
) : (
fallbackElement
)}
</Router>
</ViewTransitionContext.Provider>
<FetchersContext.Provider value={fetcherContext}>
<ViewTransitionContext.Provider value={vtContext}>
<Router
basename={basename}
location={state.location}
navigationType={state.historyAction}
navigator={navigator}
>
{state.initialized ? (
<DataRoutes routes={router.routes} state={state} />
) : (
fallbackElement
)}
</Router>
</ViewTransitionContext.Provider>
</FetchersContext.Provider>
</DataRouterStateContext.Provider>
</DataRouterContext.Provider>
{null}
Expand Down Expand Up @@ -1198,6 +1221,8 @@ enum DataRouterStateHook {
UseScrollRestoration = "useScrollRestoration",
}

// Internal hooks

function getDataRouterConsoleError(
hookName: DataRouterHook | DataRouterStateHook
) {
Expand All @@ -1216,6 +1241,49 @@ function useDataRouterState(hookName: DataRouterStateHook) {
return state;
}

function useFetcherDataLayer() {
let fetcherRefs = React.useRef<Map<string, number>>(new Map());
let fetcherData = React.useRef<Map<string, any>>(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<FetchersContextObject>(
() => ({
fetcherData: fetcherData.current,
register: registerFetcher,
unregister: unregisterFetcher,
}),
[fetcherData, registerFetcher, unregisterFetcher]
);

return { fetcherContext, fetcherData };
}

// External hooks

/**
* Handles the click behavior for router `<Link>` components. This is useful if
* you need to create custom `<Link>` components with the same click behavior we
Expand Down Expand Up @@ -1499,20 +1567,41 @@ export function useFetcher<TData = any>({
key,
}: { key?: string } = {}): FetcherWithComponents<TData> {
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<string>(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()");
Expand All @@ -1521,8 +1610,6 @@ export function useFetcher<TData = any>({
},
[fetcherKey, routeId, router]
);

// Fetcher additions (submit)
let submitImpl = useSubmit();
let submit = React.useCallback<FetcherSubmitFunction>(
(target, opts) => {
Expand All @@ -1548,31 +1635,20 @@ export function useFetcher<TData = any>({
return FetcherForm;
}, [fetcherKey]);

// Exposed FetcherWithComponents
let fetcher = router.getFetcher<TData>(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;
}

Expand Down
31 changes: 20 additions & 11 deletions packages/react-router-dom/server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -132,17 +133,25 @@ export function StaticRouterProvider({
<>
<DataRouterContext.Provider value={dataRouterContext}>
<DataRouterStateContext.Provider value={state}>
<ViewTransitionContext.Provider value={{ isTransitioning: false }}>
<Router
basename={dataRouterContext.basename}
location={state.location}
navigationType={state.historyAction}
navigator={dataRouterContext.navigator}
static={dataRouterContext.static}
>
<DataRoutes routes={router.routes} state={state} />
</Router>
</ViewTransitionContext.Provider>
<FetchersContext.Provider
value={{
fetcherData: new Map<string, any>(),
register: () => {},
unregister: () => {},
}}
>
<ViewTransitionContext.Provider value={{ isTransitioning: false }}>
<Router
basename={dataRouterContext.basename}
location={state.location}
navigationType={state.historyAction}
navigator={dataRouterContext.navigator}
static={dataRouterContext.static}
>
<DataRoutes routes={router.routes} state={state} />
</Router>
</ViewTransitionContext.Provider>
</FetchersContext.Provider>
</DataRouterStateContext.Provider>
</DataRouterContext.Provider>
{hydrateScript ? (
Expand Down

0 comments on commit cb2d911

Please sign in to comment.