Skip to content

Commit cb2d911

Browse files
authored
Add fetcher data layer (#10961)
1 parent c0dbcd2 commit cb2d911

File tree

4 files changed

+137
-47
lines changed

4 files changed

+137
-47
lines changed

.changeset/fetcher-data.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router-dom": patch
3+
---
4+
5+
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

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,10 @@
119119
"none": "16.3 kB"
120120
},
121121
"packages/react-router-dom/dist/react-router-dom.production.min.js": {
122-
"none": "15.9 kB"
122+
"none": "16.5 kB"
123123
},
124124
"packages/react-router-dom/dist/umd/react-router-dom.production.min.js": {
125-
"none": "22.1 kB"
125+
"none": "22.7 kB"
126126
}
127127
}
128128
}

packages/react-router-dom/index.tsx

Lines changed: 110 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,20 @@ if (__DEV__) {
349349

350350
export { ViewTransitionContext as UNSAFE_ViewTransitionContext };
351351

352+
// TODO: (v7) Change the useFetcher data from `any` to `unknown`
353+
type FetchersContextObject = {
354+
fetcherData: Map<string, any>;
355+
register: (key: string) => void;
356+
unregister: (key: string) => void;
357+
};
358+
359+
const FetchersContext = React.createContext<FetchersContextObject | null>(null);
360+
if (__DEV__) {
361+
FetchersContext.displayName = "Fetchers";
362+
}
363+
364+
export { FetchersContext as UNSAFE_FetchersContext };
365+
352366
//#endregion
353367

354368
////////////////////////////////////////////////////////////////////////////////
@@ -427,6 +441,7 @@ export function RouterProvider({
427441
router,
428442
future,
429443
}: RouterProviderProps): React.ReactElement {
444+
let { fetcherContext, fetcherData } = useFetcherDataLayer();
430445
let [state, setStateImpl] = React.useState(router.state);
431446
let [pendingState, setPendingState] = React.useState<RouterState>();
432447
let [vtContext, setVtContext] = React.useState<ViewTransitionContextObject>({
@@ -457,6 +472,12 @@ export function RouterProvider({
457472
newState: RouterState,
458473
{ unstable_viewTransitionOpts: viewTransitionOpts }
459474
) => {
475+
newState.fetchers.forEach((fetcher, key) => {
476+
if (fetcher.data !== undefined) {
477+
fetcherData.current.set(key, fetcher.data);
478+
}
479+
});
480+
460481
if (
461482
!viewTransitionOpts ||
462483
router.window == null ||
@@ -484,7 +505,7 @@ export function RouterProvider({
484505
});
485506
}
486507
},
487-
[optInStartTransition, transition, renderDfd, router.window]
508+
[router.window, transition, renderDfd, fetcherData, optInStartTransition]
488509
);
489510

490511
// Need to use a layout effect here so we are subscribed early enough to
@@ -587,20 +608,22 @@ export function RouterProvider({
587608
<>
588609
<DataRouterContext.Provider value={dataRouterContext}>
589610
<DataRouterStateContext.Provider value={state}>
590-
<ViewTransitionContext.Provider value={vtContext}>
591-
<Router
592-
basename={basename}
593-
location={state.location}
594-
navigationType={state.historyAction}
595-
navigator={navigator}
596-
>
597-
{state.initialized ? (
598-
<DataRoutes routes={router.routes} state={state} />
599-
) : (
600-
fallbackElement
601-
)}
602-
</Router>
603-
</ViewTransitionContext.Provider>
611+
<FetchersContext.Provider value={fetcherContext}>
612+
<ViewTransitionContext.Provider value={vtContext}>
613+
<Router
614+
basename={basename}
615+
location={state.location}
616+
navigationType={state.historyAction}
617+
navigator={navigator}
618+
>
619+
{state.initialized ? (
620+
<DataRoutes routes={router.routes} state={state} />
621+
) : (
622+
fallbackElement
623+
)}
624+
</Router>
625+
</ViewTransitionContext.Provider>
626+
</FetchersContext.Provider>
604627
</DataRouterStateContext.Provider>
605628
</DataRouterContext.Provider>
606629
{null}
@@ -1198,6 +1221,8 @@ enum DataRouterStateHook {
11981221
UseScrollRestoration = "useScrollRestoration",
11991222
}
12001223

1224+
// Internal hooks
1225+
12011226
function getDataRouterConsoleError(
12021227
hookName: DataRouterHook | DataRouterStateHook
12031228
) {
@@ -1216,6 +1241,49 @@ function useDataRouterState(hookName: DataRouterStateHook) {
12161241
return state;
12171242
}
12181243

1244+
function useFetcherDataLayer() {
1245+
let fetcherRefs = React.useRef<Map<string, number>>(new Map());
1246+
let fetcherData = React.useRef<Map<string, any>>(new Map());
1247+
1248+
let registerFetcher = React.useCallback(
1249+
(key: string) => {
1250+
let count = fetcherRefs.current.get(key);
1251+
if (count == null) {
1252+
fetcherRefs.current.set(key, 1);
1253+
} else {
1254+
fetcherRefs.current.set(key, count + 1);
1255+
}
1256+
},
1257+
[fetcherRefs]
1258+
);
1259+
1260+
let unregisterFetcher = React.useCallback(
1261+
(key: string) => {
1262+
let count = fetcherRefs.current.get(key);
1263+
if (count == null || count <= 1) {
1264+
fetcherRefs.current.delete(key);
1265+
fetcherData.current.delete(key);
1266+
} else {
1267+
fetcherRefs.current.set(key, count - 1);
1268+
}
1269+
},
1270+
[fetcherData, fetcherRefs]
1271+
);
1272+
1273+
let fetcherContext = React.useMemo<FetchersContextObject>(
1274+
() => ({
1275+
fetcherData: fetcherData.current,
1276+
register: registerFetcher,
1277+
unregister: unregisterFetcher,
1278+
}),
1279+
[fetcherData, registerFetcher, unregisterFetcher]
1280+
);
1281+
1282+
return { fetcherContext, fetcherData };
1283+
}
1284+
1285+
// External hooks
1286+
12191287
/**
12201288
* Handles the click behavior for router `<Link>` components. This is useful if
12211289
* you need to create custom `<Link>` components with the same click behavior we
@@ -1499,20 +1567,41 @@ export function useFetcher<TData = any>({
14991567
key,
15001568
}: { key?: string } = {}): FetcherWithComponents<TData> {
15011569
let { router } = useDataRouterContext(DataRouterHook.UseFetcher);
1570+
let fetchersContext = React.useContext(FetchersContext);
15021571
let route = React.useContext(RouteContext);
1503-
invariant(route, `useFetcher must be used inside a RouteContext`);
1504-
15051572
let routeId = route.matches[route.matches.length - 1]?.route.id;
1573+
1574+
invariant(
1575+
fetchersContext,
1576+
`useFetcher must be used inside a FetchersContext`
1577+
);
1578+
invariant(route, `useFetcher must be used inside a RouteContext`);
15061579
invariant(
15071580
routeId != null,
15081581
`useFetcher can only be used on routes that contain a unique "id"`
15091582
);
15101583

1584+
// Fetcher key handling
15111585
let [fetcherKey, setFetcherKey] = React.useState<string>(key || "");
15121586
if (!fetcherKey) {
15131587
setFetcherKey(getUniqueFetcherId());
15141588
}
15151589

1590+
// Registration/cleanup
1591+
let { fetcherData, register, unregister } = fetchersContext;
1592+
React.useEffect(() => {
1593+
register(fetcherKey);
1594+
return () => {
1595+
unregister(fetcherKey);
1596+
if (!router) {
1597+
console.warn(`No router available to clean up from useFetcher()`);
1598+
return;
1599+
}
1600+
router.deleteFetcher(fetcherKey);
1601+
};
1602+
}, [router, fetcherKey, register, unregister]);
1603+
1604+
// Fetcher additions
15161605
let load = React.useCallback(
15171606
(href: string) => {
15181607
invariant(router, "No router available for fetcher.load()");
@@ -1521,8 +1610,6 @@ export function useFetcher<TData = any>({
15211610
},
15221611
[fetcherKey, routeId, router]
15231612
);
1524-
1525-
// Fetcher additions (submit)
15261613
let submitImpl = useSubmit();
15271614
let submit = React.useCallback<FetcherSubmitFunction>(
15281615
(target, opts) => {
@@ -1548,31 +1635,20 @@ export function useFetcher<TData = any>({
15481635
return FetcherForm;
15491636
}, [fetcherKey]);
15501637

1638+
// Exposed FetcherWithComponents
15511639
let fetcher = router.getFetcher<TData>(fetcherKey);
1552-
1640+
let data = fetcherData.get(fetcherKey);
15531641
let fetcherWithComponents = React.useMemo(
15541642
() => ({
15551643
Form: FetcherForm,
15561644
submit,
15571645
load,
15581646
...fetcher,
1647+
data,
15591648
}),
1560-
[fetcher, FetcherForm, submit, load]
1649+
[FetcherForm, submit, load, fetcher, data]
15611650
);
15621651

1563-
React.useEffect(() => {
1564-
// Is this busted when the React team gets real weird and calls effects
1565-
// twice on mount? We really just need to garbage collect here when this
1566-
// fetcher is no longer around.
1567-
return () => {
1568-
if (!router) {
1569-
console.warn(`No router available to clean up from useFetcher()`);
1570-
return;
1571-
}
1572-
router.deleteFetcher(fetcherKey);
1573-
};
1574-
}, [router, fetcherKey]);
1575-
15761652
return fetcherWithComponents;
15771653
}
15781654

packages/react-router-dom/server.tsx

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
Router,
3535
UNSAFE_DataRouterContext as DataRouterContext,
3636
UNSAFE_DataRouterStateContext as DataRouterStateContext,
37+
UNSAFE_FetchersContext as FetchersContext,
3738
UNSAFE_ViewTransitionContext as ViewTransitionContext,
3839
} from "react-router-dom";
3940

@@ -132,17 +133,25 @@ export function StaticRouterProvider({
132133
<>
133134
<DataRouterContext.Provider value={dataRouterContext}>
134135
<DataRouterStateContext.Provider value={state}>
135-
<ViewTransitionContext.Provider value={{ isTransitioning: false }}>
136-
<Router
137-
basename={dataRouterContext.basename}
138-
location={state.location}
139-
navigationType={state.historyAction}
140-
navigator={dataRouterContext.navigator}
141-
static={dataRouterContext.static}
142-
>
143-
<DataRoutes routes={router.routes} state={state} />
144-
</Router>
145-
</ViewTransitionContext.Provider>
136+
<FetchersContext.Provider
137+
value={{
138+
fetcherData: new Map<string, any>(),
139+
register: () => {},
140+
unregister: () => {},
141+
}}
142+
>
143+
<ViewTransitionContext.Provider value={{ isTransitioning: false }}>
144+
<Router
145+
basename={dataRouterContext.basename}
146+
location={state.location}
147+
navigationType={state.historyAction}
148+
navigator={dataRouterContext.navigator}
149+
static={dataRouterContext.static}
150+
>
151+
<DataRoutes routes={router.routes} state={state} />
152+
</Router>
153+
</ViewTransitionContext.Provider>
154+
</FetchersContext.Provider>
146155
</DataRouterStateContext.Provider>
147156
</DataRouterContext.Provider>
148157
{hydrateScript ? (

0 commit comments

Comments
 (0)