diff --git a/.changeset/config.json b/.changeset/config.json index c6d1bb34ad..13e925808c 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -1,7 +1,7 @@ { "$schema": "https://unpkg.com/@changesets/config@2.0.0/schema.json", "changelog": [ - "@changesets/cli/changelog", + "@remix-run/changelog-github", { "repo": "remix-run/react-router" } ], "commit": false, diff --git a/.gitignore b/.gitignore index 6645802418..509667a660 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ node_modules/ /packages/react-router-dom-v5-compat/react-router-dom .eslintcache +/.env \ No newline at end of file diff --git a/contributors.yml b/contributors.yml index a264d1da45..d7423fe37d 100644 --- a/contributors.yml +++ b/contributors.yml @@ -3,10 +3,10 @@ - alany411 - alexlbr - AmRo045 +- andreiduca - avipatel97 - awreese - aymanemadidi -- aymanemadidi - bavardage - bhbs - BrianT1414 @@ -33,7 +33,6 @@ - fz6m - gianlucca - gijo-varghese -- gijo-varghese - goldins - gowthamvbhat - GraxMonzo @@ -47,6 +46,7 @@ - infoxicator - IsaiStormBlesed - Isammoc +- jacob-ebey - JaffParker - JakubDrozd - janpaepke @@ -68,6 +68,7 @@ - lukerSpringTree - marc2332 - markivancho +- marvinruder - maxpou - mcansh - MenouerBetty diff --git a/docs/start/overview.md b/docs/start/overview.md index 2e01502625..017ad42dc3 100644 --- a/docs/start/overview.md +++ b/docs/start/overview.md @@ -66,9 +66,11 @@ createBrowserRouter( } - loader={fetch("/api/dashboard.json", { - signal: request.signal, - })} + loader={({ request }) => + fetch("/api/dashboard.json", { + signal: request.signal, + }) + } /> }> {/* this calls back when the data is resolved */} - {(history) => } + {(resolvedHistory) => ( + + )} diff --git a/package.json b/package.json index 88ce74c07a..536bd1f31c 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "@octokit/graphql": "^4.8.0", "@octokit/plugin-paginate-rest": "^2.17.0", "@octokit/rest": "^18.12.0", + "@remix-run/changelog-github": "^0.0.5", "@remix-run/web-fetch": "4.1.3", "@rollup/plugin-babel": "^5.3.1", "@rollup/plugin-replace": "^4.0.0", diff --git a/packages/react-router-dom-v5-compat/CHANGELOG.md b/packages/react-router-dom-v5-compat/CHANGELOG.md index 080749632e..2f6350571f 100644 --- a/packages/react-router-dom-v5-compat/CHANGELOG.md +++ b/packages/react-router-dom-v5-compat/CHANGELOG.md @@ -1,8 +1,16 @@ -# react-router-dom-v5-compat +# `react-router-dom-v5-compat` + +## 6.4.1 + +### Patch Changes + +- Updated dependencies: + - `react-router@6.4.1` + - `react-router-dom@6.4.1` ## 6.4.0 **Updated dependencies** -- react-router-dom@6.4.0 -- react-router@6.4.0 +- `react-router-dom@6.4.0` +- `react-router@6.4.0` diff --git a/packages/react-router-dom-v5-compat/package.json b/packages/react-router-dom-v5-compat/package.json index 4754323e1d..a58a2e67e6 100644 --- a/packages/react-router-dom-v5-compat/package.json +++ b/packages/react-router-dom-v5-compat/package.json @@ -1,6 +1,6 @@ { "name": "react-router-dom-v5-compat", - "version": "6.4.0", + "version": "6.4.1", "description": "Migration path to React Router v6 from v4/5", "keywords": [ "react", @@ -24,12 +24,12 @@ "types": "./dist/index.d.ts", "dependencies": { "history": "^5.3.0", - "react-router": "6.4.0" + "react-router": "6.4.1" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8", - "react-router-dom": "4 || 5" + "react-router-dom": "6.4.1" }, "files": [ "dist/", diff --git a/packages/react-router-dom/CHANGELOG.md b/packages/react-router-dom/CHANGELOG.md index 0efa051205..3691be19a4 100644 --- a/packages/react-router-dom/CHANGELOG.md +++ b/packages/react-router-dom/CHANGELOG.md @@ -1,4 +1,12 @@ -# react-router-dom +# `react-router-dom` + +## 6.4.1 + +### Patch Changes + +- Updated dependencies: + - `react-router@6.4.1` + - `@remix-run/router@1.0.1` ## 6.4.0 @@ -27,7 +35,7 @@ Whoa this is a big one! `6.4.0` brings all the data loading and mutation APIs ov **Updated Dependencies** -- react-router@6.4.0 +- `react-router@6.4.0` [rr-docs]: https://reactrouter.com/ [rr-feature-overview]: https://reactrouter.com/en/6.4.0/start/overview diff --git a/packages/react-router-dom/__tests__/data-browser-router-test.tsx b/packages/react-router-dom/__tests__/data-browser-router-test.tsx index 55ba4e6ad5..fb2fcc6457 100644 --- a/packages/react-router-dom/__tests__/data-browser-router-test.tsx +++ b/packages/react-router-dom/__tests__/data-browser-router-test.tsx @@ -2270,6 +2270,93 @@ function testDomRouter( `); }); + it("handles fetcher ?index params", async () => { + let { container } = render( + + } + action={() => "PARENT ACTION"} + loader={() => "PARENT LOADER"} + > + } + action={() => "INDEX ACTION"} + loader={() => "INDEX LOADER"} + /> + + + ); + + function Index() { + let fetcher = useFetcher(); + + return ( + <> +

{fetcher.data}

+ + + + + + + + + ); + } + + async function clickAndAssert(btnText: string, expectedOutput: string) { + fireEvent.click(screen.getByText(btnText)); + await waitFor(() => screen.getByText(new RegExp(expectedOutput))); + expect(getHtml(container.querySelector("#output"))).toContain( + expectedOutput + ); + } + + await clickAndAssert("Load parent", "PARENT LOADER"); + await clickAndAssert("Load index", "INDEX LOADER"); + await clickAndAssert("Submit empty", "INDEX LOADER"); + await clickAndAssert("Submit parent get", "PARENT LOADER"); + await clickAndAssert("Submit index get", "INDEX LOADER"); + await clickAndAssert("Submit parent post", "PARENT ACTION"); + await clickAndAssert("Submit index post", "INDEX ACTION"); + }); + it("handles fetcher.load errors", async () => { let { container } = render( { `); }); + + it("preserves state from initialEntries", () => { + let renderer: TestRenderer.ReactTestRenderer; + TestRenderer.act(() => { + renderer = TestRenderer.create( + + + } /> + + + ); + }); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+        {"pathname":"/example","search":"","hash":"","state":{"my":"state"},"key":"my-key"}
+      
+ `); + }); }); diff --git a/packages/react-router/index.ts b/packages/react-router/index.ts index d2c28e39d1..64cbe78ebe 100644 --- a/packages/react-router/index.ts +++ b/packages/react-router/index.ts @@ -36,7 +36,6 @@ import { } from "@remix-run/router"; import type { - DataMemoryRouterProps, AwaitProps, MemoryRouterProps, NavigateProps, @@ -113,7 +112,6 @@ export type { ActionFunction, ActionFunctionArgs, AwaitProps, - DataMemoryRouterProps, DataRouteMatch, DataRouteObject, Fetcher, diff --git a/packages/react-router/lib/components.tsx b/packages/react-router/lib/components.tsx index 30df209dac..cd1795e95a 100644 --- a/packages/react-router/lib/components.tsx +++ b/packages/react-router/lib/components.tsx @@ -109,16 +109,6 @@ export function RouterProvider({ ); } -export interface DataMemoryRouterProps { - basename?: string; - children?: React.ReactNode; - initialEntries?: InitialEntry[]; - initialIndex?: number; - hydrationData?: HydrationState; - fallbackElement?: React.ReactNode; - routes?: RouteObject[]; -} - export interface MemoryRouterProps { basename?: string; children?: React.ReactNode; diff --git a/packages/react-router/package.json b/packages/react-router/package.json index fba2303947..afeeff80a5 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -1,6 +1,6 @@ { "name": "react-router", - "version": "6.4.0", + "version": "6.4.1", "description": "Declarative routing for React", "keywords": [ "react", @@ -23,7 +23,7 @@ "module": "./dist/index.js", "types": "./dist/index.d.ts", "dependencies": { - "@remix-run/router": "1.0.0" + "@remix-run/router": "1.0.1" }, "devDependencies": { "react": "^18.2.0" diff --git a/packages/router/CHANGELOG.md b/packages/router/CHANGELOG.md index c5261bae55..e592bd83b2 100644 --- a/packages/router/CHANGELOG.md +++ b/packages/router/CHANGELOG.md @@ -1,4 +1,11 @@ -# @remix-run/router +# `@remix-run/router` + +## 1.0.1 + +### Patch Changes + +- Preserve state from `initialEntries` ([#9288](https://github.com/remix-run/react-router/pull/9288)) +- Preserve `?index` for fetcher get submissions to index routes ([#9312](https://github.com/remix-run/react-router/pull/9312)) ## 1.0.0 @@ -6,7 +13,7 @@ This is the first stable release of `@remix-run/router`, which provides all the For an overview of the features provided by `react-router`, we recommend you go check out the [docs][rr-docs], especially the [feature overview][rr-feature-overview] and the [tutorial][rr-tutorial]. -For an overview of the features provided by `@remix-run/router`, please check out the [README][remix-router-readme]. +For an overview of the features provided by `@remix-run/router`, please check out the [`README`][remix-router-readme]. [rr-docs]: https://reactrouter.com/ [rr-feature-overview]: https://reactrouter.com/en/6.4.0/start/overview diff --git a/packages/router/__tests__/memory-test.ts b/packages/router/__tests__/memory-test.ts index d724199a95..e5233b50e3 100644 --- a/packages/router/__tests__/memory-test.ts +++ b/packages/router/__tests__/memory-test.ts @@ -173,4 +173,32 @@ describe("a memory history with some initial entries", () => { key: expect.any(String), }); }); + + it("allows initial entries to have state and keys", () => { + let history = createMemoryHistory({ + initialEntries: [ + { pathname: "/one", state: "1", key: "1" }, + { pathname: "/two", state: "2", key: "2" }, + ], + }); + + expect(history.index).toBe(1); + expect(history.location).toMatchObject({ + pathname: "/two", + search: "", + hash: "", + state: "2", + key: "2", + }); + + history.go(-1); + expect(history.index).toBe(0); + expect(history.location).toMatchObject({ + pathname: "/one", + search: "", + hash: "", + state: "1", + key: "1", + }); + }); }); diff --git a/packages/router/__tests__/router-test.ts b/packages/router/__tests__/router-test.ts index 1d4e7f9dab..036a1a3042 100644 --- a/packages/router/__tests__/router-test.ts +++ b/packages/router/__tests__/router-test.ts @@ -7605,6 +7605,76 @@ describe("a router", () => { }); }); }); + + describe("fetcher ?index params", () => { + it("hits the proper Routes when ?index params are present", async () => { + let t = setup({ + routes: [ + { + id: "parent", + path: "parent", + action: true, + loader: true, + // Turn off revalidation after fetcher action submission for this test + shouldRevalidate: () => false, + children: [ + { + id: "index", + index: true, + action: true, + loader: true, + // Turn off revalidation after fetcher action submission for this test + shouldRevalidate: () => false, + }, + ], + }, + ], + initialEntries: ["/parent"], + hydrationData: { loaderData: { parent: "PARENT", index: "INDEX" } }, + }); + + let key = "KEY"; + + // fetcher.load() + let A = await t.fetch("/parent", key); + await A.loaders.parent.resolve("PARENT LOADER"); + expect(t.router.getFetcher(key).data).toBe("PARENT LOADER"); + + let B = await t.fetch("/parent?index", key); + await B.loaders.index.resolve("INDEX LOADER"); + expect(t.router.getFetcher(key).data).toBe("INDEX LOADER"); + + // fetcher.submit({}, { method: 'get' }) + let C = await t.fetch("/parent", key, { + formMethod: "get", + formData: createFormData({}), + }); + await C.loaders.parent.resolve("PARENT LOADER"); + expect(t.router.getFetcher(key).data).toBe("PARENT LOADER"); + + let D = await t.fetch("/parent?index", key, { + formMethod: "get", + formData: createFormData({}), + }); + await D.loaders.index.resolve("INDEX LOADER"); + expect(t.router.getFetcher(key).data).toBe("INDEX LOADER"); + + // fetcher.submit({}, { method: 'post' }) + let E = await t.fetch("/parent", key, { + formMethod: "post", + formData: createFormData({}), + }); + await E.actions.parent.resolve("PARENT ACTION"); + expect(t.router.getFetcher(key).data).toBe("PARENT ACTION"); + + let F = await t.fetch("/parent?index", key, { + formMethod: "post", + formData: createFormData({}), + }); + await F.actions.index.resolve("INDEX ACTION"); + expect(t.router.getFetcher(key).data).toBe("INDEX ACTION"); + }); + }); }); describe("deferred data", () => { diff --git a/packages/router/history.ts b/packages/router/history.ts index 3a8d0dbce0..ce6d0c1fca 100644 --- a/packages/router/history.ts +++ b/packages/router/history.ts @@ -208,7 +208,11 @@ export function createMemoryHistory( let { initialEntries = ["/"], initialIndex, v5Compat = false } = options; let entries: Location[]; // Declare so we can access from createMemoryLocation entries = initialEntries.map((entry, index) => - createMemoryLocation(entry, null, index === 0 ? "default" : undefined) + createMemoryLocation( + entry, + typeof entry === "string" ? null : entry.state, + index === 0 ? "default" : undefined + ) ); let index = clampIndex( initialIndex == null ? entries.length - 1 : initialIndex @@ -325,8 +329,8 @@ export function createBrowserHistory( "", { pathname, search, hash }, // state defaults to `null` because `window.history.state` does - globalHistory.state?.usr || null, - globalHistory.state?.key || "default" + (globalHistory.state && globalHistory.state.usr) || null, + (globalHistory.state && globalHistory.state.key) || "default" ); } @@ -386,8 +390,8 @@ export function createHashHistory( "", { pathname, search, hash }, // state defaults to `null` because `window.history.state` does - globalHistory.state?.usr || null, - globalHistory.state?.key || "default" + (globalHistory.state && globalHistory.state.usr) || null, + (globalHistory.state && globalHistory.state.key) || "default" ); } @@ -476,7 +480,7 @@ export function createLocation( // full Locations now and avoid the need to run through this flow at all // But that's a pretty big refactor to the current test suite so going to // keep as is for the time being and just let any incoming keys take precedence - key: (to as Location)?.key || key || createKey(), + key: (to && (to as Location).key) || key || createKey(), }; return location; } @@ -551,7 +555,7 @@ function getUrlBasedHistory( function push(to: To, state?: any) { action = Action.Push; let location = createLocation(history.location, to, state); - validateLocation?.(location, to); + if (validateLocation) validateLocation(location, to); let historyState = getHistoryState(location); let url = history.createHref(location); @@ -573,7 +577,7 @@ function getUrlBasedHistory( function replace(to: To, state?: any) { action = Action.Replace; let location = createLocation(history.location, to, state); - validateLocation?.(location, to); + if (validateLocation) validateLocation(location, to); let historyState = getHistoryState(location); let url = history.createHref(location); diff --git a/packages/router/package.json b/packages/router/package.json index 752e09ace2..5fa610251e 100644 --- a/packages/router/package.json +++ b/packages/router/package.json @@ -1,6 +1,6 @@ { "name": "@remix-run/router", - "version": "1.0.0", + "version": "1.0.1", "description": "Nested/Data-driven/Framework-agnostic Routing", "keywords": [ "remix", diff --git a/packages/router/router.ts b/packages/router/router.ts index 1532ba67b1..6af50bef5b 100644 --- a/packages/router/router.ts +++ b/packages/router/router.ts @@ -555,9 +555,9 @@ export function createRouter(init: RouterInit): Router { restoreScrollPosition: null, preventScrollReset: false, revalidation: "idle", - loaderData: init.hydrationData?.loaderData || {}, - actionData: init.hydrationData?.actionData || null, - errors: init.hydrationData?.errors || initialErrors, + loaderData: (init.hydrationData && init.hydrationData.loaderData) || {}, + actionData: (init.hydrationData && init.hydrationData.actionData) || null, + errors: (init.hydrationData && init.hydrationData.errors) || initialErrors, fetchers: new Map(), }; @@ -628,7 +628,7 @@ export function createRouter(init: RouterInit): Router { unlistenHistory(); } subscribers.clear(); - pendingNavigationController?.abort(); + pendingNavigationController && pendingNavigationController.abort(); state.fetchers.forEach((_, key) => deleteFetcher(key)); } @@ -731,9 +731,9 @@ export function createRouter(init: RouterInit): Router { let { path, submission, error } = normalizeNavigateOptions(to, opts); - let location = createLocation(state.location, path, opts?.state); + let location = createLocation(state.location, path, opts && opts.state); let historyAction = - opts?.replace === true || submission != null + (opts && opts.replace) === true || submission != null ? HistoryAction.Replace : HistoryAction.Push; let preventScrollReset = @@ -747,7 +747,7 @@ export function createRouter(init: RouterInit): Router { // render at the right error boundary after we match routes pendingError: error, preventScrollReset, - replace: opts?.replace, + replace: opts && opts.replace, }); } @@ -802,17 +802,18 @@ export function createRouter(init: RouterInit): Router { // Abort any in-progress navigations and start a new one. Unset any ongoing // uninterrupted revalidations unless told otherwise, since we want this // new navigation to update history normally - pendingNavigationController?.abort(); + pendingNavigationController && pendingNavigationController.abort(); pendingNavigationController = null; pendingAction = historyAction; - isUninterruptedRevalidation = opts?.startUninterruptedRevalidation === true; + isUninterruptedRevalidation = + (opts && opts.startUninterruptedRevalidation) === true; // Save the current scroll position every time we start a new navigation, // and track whether we should reset scroll on completion saveScrollPosition(state.location, state.matches); - pendingPreventScrollReset = opts?.preventScrollReset === true; + pendingPreventScrollReset = (opts && opts.preventScrollReset) === true; - let loadingNavigation = opts?.overrideNavigation; + let loadingNavigation = opts && opts.overrideNavigation; let matches = matchRoutes(dataRoutes, location, init.basename); // Short circuit with a 404 on the root error boundary if we match nothing @@ -845,12 +846,12 @@ export function createRouter(init: RouterInit): Router { let request = createRequest( location, pendingNavigationController.signal, - opts?.submission + opts && opts.submission ); let pendingActionData: RouteData | undefined; let pendingError: RouteData | undefined; - if (opts?.pendingError) { + if (opts && opts.pendingError) { // If we have a pendingError, it means the user attempted a GET submission // with binary FormData so assign here and skip to handleLoaders. That // way we handle calling loaders above the boundary etc. It's not really @@ -858,7 +859,7 @@ export function createRouter(init: RouterInit): Router { pendingError = { [findNearestBoundary(matches).route.id]: opts.pendingError, }; - } else if (opts?.submission) { + } else if (opts && opts.submission) { // Call action if we received an action submission let actionOutput = await handleAction( request, @@ -889,8 +890,8 @@ export function createRouter(init: RouterInit): Router { location, matches, loadingNavigation, - opts?.submission, - opts?.replace, + opts && opts.submission, + opts && opts.replace, pendingActionData, pendingError ); @@ -950,7 +951,11 @@ export function createRouter(init: RouterInit): Router { location: createLocation(state.location, result.location), ...submission, }; - await startRedirectNavigation(result, redirectNavigation, opts?.replace); + await startRedirectNavigation( + result, + redirectNavigation, + opts && opts.replace + ); return { shortCircuited: true }; } @@ -963,7 +968,7 @@ export function createRouter(init: RouterInit): Router { // action threw an error that'll be rendered in an errorElement, we fall // back to PUSH so that the user can use the back button to get back to // the pre-submission form location to try again - if (opts?.replace !== true) { + if ((opts && opts.replace) !== true) { pendingAction = HistoryAction.Push; } @@ -1025,8 +1030,8 @@ export function createRouter(init: RouterInit): Router { // already cancelled all pending deferreds so this would be a no-op cancelActiveDeferreds( (routeId) => - !matches?.some((m) => m.route.id === routeId) || - matchesToLoad?.some((m) => m.route.id === routeId) + !(matches && matches.some((m) => m.route.id === routeId)) || + (matchesToLoad && matchesToLoad.some((m) => m.route.id === routeId)) ); // Short circuit if we have no loaders to run @@ -1047,9 +1052,10 @@ export function createRouter(init: RouterInit): Router { // a revalidation interrupting an actionReload) if (!isUninterruptedRevalidation) { revalidatingFetchers.forEach(([key]) => { + const fetcher = state.fetchers.get(key); let revalidatingFetcher: FetcherStates["Loading"] = { state: "loading", - data: state.fetchers.get(key)?.data, + data: fetcher && fetcher.data, formMethod: undefined, formAction: undefined, formEncType: undefined, @@ -1159,7 +1165,7 @@ export function createRouter(init: RouterInit): Router { return; } - let { path, submission } = normalizeNavigateOptions(href, opts); + let { path, submission } = normalizeNavigateOptions(href, opts, true); let match = getTargetMatch(matches, path); if (submission) { @@ -1192,10 +1198,11 @@ export function createRouter(init: RouterInit): Router { } // Put this fetcher into it's submitting state + let existingFetcher = state.fetchers.get(key); let fetcher: FetcherStates["Submitting"] = { state: "submitting", ...submission, - data: state.fetchers.get(key)?.data || undefined, + data: existingFetcher && existingFetcher.data, }; state.fetchers.set(key, fetcher); updateState({ fetchers: new Map(state.fetchers) }); @@ -1289,9 +1296,10 @@ export function createRouter(init: RouterInit): Router { revalidatingFetchers .filter(([staleKey]) => staleKey !== key) .forEach(([staleKey]) => { + let existingFetcher = state.fetchers.get(staleKey); let revalidatingFetcher: FetcherStates["Loading"] = { state: "loading", - data: state.fetchers.get(staleKey)?.data, + data: existingFetcher && existingFetcher.data, formMethod: undefined, formAction: undefined, formEncType: undefined, @@ -1360,7 +1368,7 @@ export function createRouter(init: RouterInit): Router { loadId > pendingNavigationLoadId ) { invariant(pendingAction, "Expected pending action"); - pendingNavigationController?.abort(); + pendingNavigationController && pendingNavigationController.abort(); completeNavigation(state.navigation.location, { matches, @@ -1388,6 +1396,7 @@ export function createRouter(init: RouterInit): Router { path: string, match: AgnosticDataRouteMatch ) { + let existingFetcher = state.fetchers.get(key); // Put this fetcher into it's loading state let loadingFetcher: FetcherStates["Loading"] = { state: "loading", @@ -1395,7 +1404,7 @@ export function createRouter(init: RouterInit): Router { formAction: undefined, formEncType: undefined, formData: undefined, - data: state.fetchers.get(key)?.data || undefined, + data: existingFetcher && existingFetcher.data, }; state.fetchers.set(key, loadingFetcher); updateState({ fetchers: new Map(state.fetchers) }); @@ -2089,7 +2098,8 @@ export function getStaticContextFromError( // URLSearchParams so they behave identically to links with query params function normalizeNavigateOptions( to: To, - opts?: RouterNavigateOptions + opts?: RouterNavigateOptions, + isFetcher = false ): { path: string; submission?: Submission; @@ -2109,7 +2119,8 @@ function normalizeNavigateOptions( submission: { formMethod: opts.formMethod, formAction: createHref(parsePath(path)), - formEncType: opts?.formEncType || "application/x-www-form-urlencoded", + formEncType: + (opts && opts.formEncType) || "application/x-www-form-urlencoded", formData: opts.formData, }, }; @@ -2124,6 +2135,16 @@ function normalizeNavigateOptions( let parsedPath = parsePath(path); try { let searchParams = convertFormDataToSearchParams(opts.formData); + // Since fetcher GET submissions only run a single loader (as opposed to + // navigation GET submissions which run all loaders), we need to preserve + // any incoming ?index params + if ( + isFetcher && + parsedPath.search && + hasNakedIndexQuery(parsedPath.search) + ) { + searchParams.append("index", ""); + } parsedPath.search = `?${searchParams}`; } catch (e) { return { @@ -2211,25 +2232,26 @@ function getMatchesToLoad( // Pick fetcher.loads that need to be revalidated let revalidatingFetchers: RevalidatingFetcher[] = []; - fetchLoadMatches?.forEach(([href, match], key) => { - // This fetcher was cancelled from a prior action submission - force reload - if (cancelledFetcherLoads.includes(key)) { - revalidatingFetchers.push([key, href, match]); - } else if (isRevalidationRequired) { - let shouldRevalidate = shouldRevalidateLoader( - href, - match, - submission, - href, - match, - isRevalidationRequired, - actionResult - ); - if (shouldRevalidate) { + fetchLoadMatches && + fetchLoadMatches.forEach(([href, match], key) => { + // This fetcher was cancelled from a prior action submission - force reload + if (cancelledFetcherLoads.includes(key)) { revalidatingFetchers.push([key, href, match]); + } else if (isRevalidationRequired) { + let shouldRevalidate = shouldRevalidateLoader( + href, + match, + submission, + href, + match, + isRevalidationRequired, + actionResult + ); + if (shouldRevalidate) { + revalidatingFetchers.push([key, href, match]); + } } - } - }); + }); return [navigationMatches, revalidatingFetchers]; } @@ -2257,12 +2279,14 @@ function isNewRouteInstance( currentMatch: AgnosticDataRouteMatch, match: AgnosticDataRouteMatch ) { + let currentPath = currentMatch.route.path; return ( // param change for this match, /users/123 -> /users/456 currentMatch.pathname !== match.pathname || // splat param changed, which is not present in match.path // e.g. /files/images/avatar.jpg -> files/finances.xls - (currentMatch.route.path?.endsWith("*") && + (currentPath && + currentPath.endsWith("*") && currentMatch.params["*"] !== match.params["*"]) ); } @@ -2375,7 +2399,8 @@ async function callLoaderOrAction( } let data: any; - if (result.headers.get("Content-Type")?.startsWith("application/json")) { + let contentType = result.headers.get("Content-Type"); + if (contentType && contentType.startsWith("application/json")) { data = await result.json(); } else { data = await result.text(); @@ -2497,7 +2522,7 @@ function processRouteLoaderData( loaderHeaders[id] = result.headers; } } else if (isDeferredResult(result)) { - activeDeferreds?.set(id, result.deferredData); + activeDeferreds && activeDeferreds.set(id, result.deferredData); loaderData[id] = result.deferredData.data; // TODO: Add statusCode/headers once we wire up streaming in Remix } else { @@ -2560,7 +2585,7 @@ function processLoaderData( // Process fetcher non-redirect errors if (isErrorResult(result)) { let boundaryMatch = findNearestBoundary(state.matches, match.route.id); - if (!errors?.[boundaryMatch.route.id]) { + if (!(errors && errors[boundaryMatch.route.id])) { errors = { ...errors, [boundaryMatch.route.id]: result.error, @@ -2695,7 +2720,7 @@ function isErrorResult(result: DataResult): result is ErrorResult { } function isRedirectResult(result?: DataResult): result is RedirectResult { - return result?.type === ResultType.redirect; + return (result && result.type) === ResultType.redirect; } async function resolveDeferredResults( @@ -2715,7 +2740,7 @@ async function resolveDeferredResults( let isRevalidatingLoader = currentMatch != null && !isNewRouteInstance(currentMatch, match) && - currentLoaderData?.[match.route.id] !== undefined; + (currentLoaderData && currentLoaderData[match.route.id]) !== undefined; if (isDeferredResult(result) && (isFetcher || isRevalidatingLoader)) { // Note: we do not have to touch activeDeferreds here since we race them diff --git a/packages/router/utils.ts b/packages/router/utils.ts index eb73f90673..cb3e89b85d 100644 --- a/packages/router/utils.ts +++ b/packages/router/utils.ts @@ -986,14 +986,15 @@ export class DeferredData { this.unlistenAbortSignal(); } + const subscriber = this.subscriber; if (error) { Object.defineProperty(promise, "_error", { get: () => error }); - this.subscriber?.(false); + subscriber && subscriber(false); return Promise.reject(error); } Object.defineProperty(promise, "_data", { get: () => data }); - this.subscriber?.(false); + subscriber && subscriber(false); return data; } @@ -1004,7 +1005,8 @@ export class DeferredData { cancel() { this.controller.abort(); this.pendingKeys.forEach((v, k) => this.pendingKeys.delete(k)); - this.subscriber?.(true); + let subscriber = this.subscriber; + subscriber && subscriber(true); } async resolveData(signal: AbortSignal) { diff --git a/yarn.lock b/yarn.lock index 8ce6ebf098..0cb33bfbef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1216,7 +1216,7 @@ fs-extra "^7.0.1" semver "^5.4.1" -"@changesets/get-github-info@^0.5.0": +"@changesets/get-github-info@^0.5.0", "@changesets/get-github-info@^0.5.1": version "0.5.1" resolved "https://registry.yarnpkg.com/@changesets/get-github-info/-/get-github-info-0.5.1.tgz#5a20328b26f301b2193717abb32e73651e8811b7" integrity sha512-w2yl3AuG+hFuEEmT6j1zDlg7GQLM/J2UxTmk0uJBMdRqHni4zXGe/vUlPfLom5KfX3cRfHc0hzGvloDPjWFNZw== @@ -1950,6 +1950,16 @@ wcwidth "^1.0.1" ws "^1.1.0" +"@remix-run/changelog-github@^0.0.5": + version "0.0.5" + resolved "https://registry.yarnpkg.com/@remix-run/changelog-github/-/changelog-github-0.0.5.tgz#d0eecaea45102b35b1c8fd41bb74fd1c38f7f70f" + integrity sha512-43tqwUqWqirbv6D9uzo55ASPsCJ61Ein1k/M8qn+Qpros0MmbmuzjLVPmtaxfxfe2ANX0LefLvCD0pAgr1tp4g== + dependencies: + "@changesets/errors" "^0.1.4" + "@changesets/get-github-info" "^0.5.1" + "@changesets/types" "^5.0.0" + dotenv "^8.1.0" + "@remix-run/web-blob@^3.0.4": version "3.0.4" resolved "https://registry.yarnpkg.com/@remix-run/web-blob/-/web-blob-3.0.4.tgz#99c67b9d0fb641bd0c07d267fd218ae5aa4ae5ed"