-
-
Notifications
You must be signed in to change notification settings - Fork 10.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add fetcher data layer #10961
Add fetcher data layer #10961
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
||
//////////////////////////////////////////////////////////////////////////////// | ||
|
@@ -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>({ | ||
|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fetcher data hand-off from the router to the React data layer |
||
} | ||
}); | ||
|
||
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({ | |
<> | ||
<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} | ||
|
@@ -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<Map<string, number>>(new Map()); | ||
let fetcherData = React.useRef<Map<string, any>>(new Map()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we're OK using a ref here but I need to bang on some edge cases to be sure - all tests and such pass as-is which is reassuring. The idea is that anytime a |
||
|
||
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 | ||
|
@@ -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()"); | ||
|
@@ -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) => { | ||
|
@@ -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; | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fetcher data lives in the React layer now, and we use ref counting via
register
/unregister
to know when to remove fromfetcherData