Skip to content
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

Lazy Loaded Route Modules #10045

Merged
merged 68 commits into from
Mar 8, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
566672b
POC of lazily loaded route modules
brophdawg11 Feb 2, 2023
36975ff
Update logic and add Marks examples
brophdawg11 Feb 3, 2023
1d4439d
Update createRoutesFromChildren tests to reflect new lazy property
brophdawg11 Feb 3, 2023
971172f
1kb bundle bump
brophdawg11 Feb 3, 2023
c15b7cb
Add route mapper
markdalgleish Feb 7, 2023
47d09a8
Replace routeMapper with hasErrorBoundary function
markdalgleish Feb 7, 2023
77060e4
Add markdalgleish to contributors
markdalgleish Feb 7, 2023
af16b4b
Minor updates
brophdawg11 Feb 7, 2023
6284984
Add onInitialize callback
markdalgleish Feb 8, 2023
3e6039a
Unit test harness updates
brophdawg11 Feb 8, 2023
68a143b
Add lazy route error handling tests
markdalgleish Feb 8, 2023
661eb4c
Fix lazy actions after navigation, add more tests
markdalgleish Feb 9, 2023
f86e58e
Remove unused type import
markdalgleish Feb 9, 2023
e7ad8f8
Minor cleanups
brophdawg11 Feb 9, 2023
a6b49c6
DRY up typings with LazyRouteFunction<R>
brophdawg11 Feb 9, 2023
54af8c0
Fix lazy() handling on aborted requests
brophdawg11 Feb 9, 2023
e8daf54
Update changeset
brophdawg11 Feb 9, 2023
f94adc5
Merge branch 'dev' into brophdawg11/lazy-route-modules
brophdawg11 Feb 9, 2023
7c8f3a4
Bump bundle
brophdawg11 Feb 9, 2023
29a01f6
Fix typos in changeset
markdalgleish Feb 9, 2023
4fdc3cc
Keep resolved lazy routes even after cancellation
markdalgleish Feb 10, 2023
84369ad
Ensure immutable route key references are coupled
markdalgleish Feb 10, 2023
a708858
Clean up stray space character in example JSX
markdalgleish Feb 10, 2023
fcb59cd
Move comment
markdalgleish Feb 10, 2023
63d49ec
Add docs, tweak changeset
markdalgleish Feb 10, 2023
68c67c8
update changeset with proposal and POC implementation
brophdawg11 Feb 10, 2023
5d3b172
Ensure static route props take priority over lazy
markdalgleish Feb 13, 2023
0ea4435
Exclude hasErrorBoundary from static route props
markdalgleish Feb 13, 2023
112c4fd
Bump router bundle size
markdalgleish Feb 13, 2023
5049aea
Trim down `lazy` docs at route level
markdalgleish Feb 13, 2023
1dd74ee
Run lazy actions after cancellation on lazy load
markdalgleish Feb 13, 2023
b688d43
Add initial react-router-dom test cases
markdalgleish Feb 13, 2023
39e6522
Add `ready` function, add more RR tests, fix types
markdalgleish Feb 14, 2023
890cab3
WIP lazy load error handling
markdalgleish Feb 16, 2023
ab8bd20
Partial Revert "WIP lazy load error handling"
brophdawg11 Feb 17, 2023
88431bc
Move route.lazy() execution into callLoaderOrAction
brophdawg11 Feb 17, 2023
508614c
Merge branch 'dev' into brophdawg11/lazy-route-modules
brophdawg11 Feb 17, 2023
5656963
Rename/update changeset
brophdawg11 Feb 21, 2023
0826818
Remove uneeded empty abortPromise.catch
brophdawg11 Feb 21, 2023
e673bb0
Extract to standalone lazy example
brophdawg11 Feb 21, 2023
938d8db
Merge branch 'dev' into brophdawg11/lazy-route-modules
brophdawg11 Feb 21, 2023
7f9b7b5
Fix tests
brophdawg11 Feb 21, 2023
bbfbf85
Remove __DEV__ configs from router lint/jest config
brophdawg11 Feb 21, 2023
95974d9
Add decision doc
brophdawg11 Feb 21, 2023
ca8685c
Call loader even if lazy() is aborted, like action
brophdawg11 Feb 23, 2023
c821784
Update decision doc
brophdawg11 Feb 23, 2023
664464a
Update comments and re-organize tests
brophdawg11 Feb 23, 2023
caed6bf
hasErrorBoundary function -> detectErrorBoundary
brophdawg11 Feb 23, 2023
5d329ae
Add support for route Component/ErrorBoundary props
brophdawg11 Feb 23, 2023
87f8bf0
Remove router.ready() in favor of resolveLazyRoutes() utility
brophdawg11 Feb 27, 2023
0886eb4
Remove server ready() and update decision doc
brophdawg11 Feb 27, 2023
de84075
Revert "Remove server ready() and update decision doc"
brophdawg11 Mar 1, 2023
3fe87ed
Revert "Remove router.ready() in favor of resolveLazyRoutes() utility"
brophdawg11 Mar 1, 2023
be19639
Remove ready() and update tests/docs
brophdawg11 Mar 1, 2023
15e46d9
Merge branch 'dev' into brophdawg11/lazy-route-modules
brophdawg11 Mar 1, 2023
1e309fd
Add more tests for Component/ErrorBoundary
brophdawg11 Mar 1, 2023
38d9f06
Update docs with notes on Component/ErrorBoundary
brophdawg11 Mar 1, 2023
4430768
Update docs
brophdawg11 Mar 1, 2023
b6c4d7d
Minor updates
brophdawg11 Mar 1, 2023
b72d4f2
Optimize execution of static handlers in parallel with lazy
brophdawg11 Mar 2, 2023
629e423
Fix static router test case
brophdawg11 Mar 2, 2023
a4b12e9
Bundle bump
brophdawg11 Mar 2, 2023
6f8d08f
Fix typos
brophdawg11 Mar 3, 2023
d8fe279
Update to docs
brophdawg11 Mar 3, 2023
7b5dc38
Few more typos
brophdawg11 Mar 3, 2023
2c9f13d
Change typing from FunctionComponent -> ComponentType
brophdawg11 Mar 6, 2023
6d79d3d
Bump bundle
brophdawg11 Mar 7, 2023
2560422
Final docs upates
brophdawg11 Mar 8, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/many-frogs-accept.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"react-router": minor
"react-router-dom": minor
"@remix-run/router": minor
---

Add support for `route.lazy` for code-splitting and lazy-loading of route modules
63 changes: 63 additions & 0 deletions examples/data-router/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,40 @@ let router = createBrowserRouter(
loader={deferredLoader}
element={<DeferredPage />}
/>
<Route
path="lazy"
lazy={async () => {
console.log("loading lazy");
await sleep(1000);
console.log("done loading lazy");
let {
default: Component,
loader,
action,
ErrorBoundary,
shouldRevalidate,
} = await import("./lazy");

return {
element: <Component />,
loader,
action,
shouldRevalidate,
...(ErrorBoundary
? {
errorElement: <ErrorBoundary />,
hasErrorBoundary: true,
}
: {}),
};
}}
/>
</Route>
)
);

window.router = router;
brophdawg11 marked this conversation as resolved.
Show resolved Hide resolved

if (import.meta.hot) {
import.meta.hot.dispose(() => router.dispose());
}
Expand All @@ -68,6 +98,7 @@ export function Fallback() {
export function Layout() {
let navigation = useNavigation();
let revalidator = useRevalidator();
let lazyFetcher = useFetcher();
let fetchers = useFetchers();
let fetcherInProgress = fetchers.some((f) =>
["loading", "submitting"].includes(f.state)
Expand All @@ -94,14 +125,46 @@ export function Layout() {
<li>
<Link to="/deferred">Deferred</Link>
</li>
<li>
<Link to="/lazy">Lazy</Link>
</li>{" "}
<li>
<Link to="/404">404 Link</Link>
</li>
<li>
<Form method="post" action="/lazy">
<button type="submit">Post to /lazy</button>
</Form>
</li>
<li>
<button onClick={() => revalidator.revalidate()}>
Revalidate Data
</button>
</li>
<li>
<button onClick={() => lazyFetcher.load("/lazy")}>
Load /lazy via fetcher
</button>
&nbsp;&nbsp;
<span>
fetcher state/data: {lazyFetcher.state}/
{JSON.stringify(lazyFetcher.data)}
</span>
</li>
<li>
<button
onClick={() =>
lazyFetcher.submit({}, { method: "post", action: "/lazy" })
}
>
Submit to /lazy via fetcher
</button>
&nbsp;&nbsp;
<span>
fetcher state/data: {lazyFetcher.state}/
{JSON.stringify(lazyFetcher.data)}
</span>
</li>
</ul>
</nav>
<div style={{ position: "fixed", top: 0, right: 0 }}>
Expand Down
70 changes: 70 additions & 0 deletions examples/data-router/src/lazy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React from "react";
import type {
ActionFunction,
ShouldRevalidateFunction,
} from "react-router-dom";
import { Form, useLoaderData } from "react-router-dom";

interface LazyLoaderData {
date: string;
submissionCount: number;
}

let submissionCount = 0;

export const loader = async (): Promise<LazyLoaderData> => {
console.log("lazy loader start");
await new Promise((r) => setTimeout(r, 1000));
console.log("lazy loader end");
return {
date: new Date().toISOString(),
submissionCount,
};
};

export const action: ActionFunction = async ({ request }) => {
console.log("lazy action start");
await new Promise((r) => setTimeout(r, 1000));
console.log("lazy action end");

let body = await request.formData();
if (body.get("error")) {
throw new Error("Form action error");
}

submissionCount++;
return submissionCount;
};

export function ErrorBoundary() {
return (
<>
<h2>Lazy error boundary</h2>
<pre>Something went wrong</pre>
</>
);
}

export const shouldRevalidate: ShouldRevalidateFunction = (args) => {
return Boolean(args.formAction);
};

export default function LazyPage() {
let data = useLoaderData() as LazyLoaderData;

return (
<>
<h2>Lazy</h2>
<p>Date from loader: {data.date}</p>
<p>Form submission count: {data.submissionCount}</p>
<Form method="post">
<div style={{ display: "flex", gap: 12 }}>
<button>Submit form</button>
<button name="error" value="true">
Throw an error
</button>
</div>
</Form>
</>
);
}
50 changes: 26 additions & 24 deletions examples/ssr-data-router/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions examples/ssr-data-router/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@
},
"dependencies": {
"@remix-run/node": "^1.7.0",
"@remix-run/router": "^1.0.0",
"@remix-run/router": "^1.3.1",
"compression": "1.7.4",
"cross-env": "^7.0.3",
"express": "^4.17.1",
"history": "^5.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.4.0"
"react-router-dom": "^6.8.0"
},
"devDependencies": {
"@rollup/plugin-replace": "^3.0.0",
Expand Down
33 changes: 32 additions & 1 deletion examples/ssr-data-router/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,34 @@ export const routes = [
loader: dashboardLoader,
element: <Dashboard />,
},
{
path: "lazy",
async lazy() {
console.log("start lazy()");
await sleep(1000);
console.log("end lazy()");
let {
default: Component,
loader,
action,
ErrorBoundary,
shouldRevalidate,
} = await import("./lazy");

return {
element: <Component />,
loader,
action,
shouldRevalidate,
...(ErrorBoundary
? {
errorElement: <ErrorBoundary />,
hasErrorBoundary: true,
}
: {}),
};
},
},
{
path: "redirect",
loader: redirectLoader,
Expand Down Expand Up @@ -70,6 +98,9 @@ function Layout() {
<li>
<Link to="/dashboard">Dashboard</Link>
</li>
<li>
<Link to="/lazy">Lazy</Link>
</li>
<li>
<Link to="/redirect">Redirect to Home</Link>
</li>
Expand All @@ -86,7 +117,7 @@ function Layout() {
);
}

const sleep = () => new Promise((r) => setTimeout(r, 500));
const sleep = (n = 500) => new Promise((r) => setTimeout(r, n));
const rand = () => Math.round(Math.random() * 100);

async function homeLoader() {
Expand Down
34 changes: 28 additions & 6 deletions examples/ssr-data-router/src/entry.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,31 @@ import { routes } from "./App";

let router = createBrowserRouter(routes);

ReactDOM.hydrateRoot(
document.getElementById("app"),
<React.StrictMode>
<RouterProvider router={router} fallbackElement={null} />
</React.StrictMode>
);
// If you're using lazy route modules and you haven't yet preloaded them onto
// routes, then you'll need to wait for the router to be initialized before
// hydrating, since it will have initial data to hydrate but it won't yet have
// any router elements to render.
//
// This shouldn't be needed in most SSR stacks as you should know what routes
// are initially rendered and be able to SSR the appropriate <Script> tags for
// those modules such that they're readily available on your client-side route
// definitions
if (!router.state.initialized) {
let unsub = router.subscribe((state) => {
if (state.initialized) {
unsub();
hydrate();
}
});
} else {
hydrate();
}
brophdawg11 marked this conversation as resolved.
Show resolved Hide resolved

function hydrate() {
ReactDOM.hydrateRoot(
document.getElementById("app"),
<React.StrictMode>
<RouterProvider router={router} fallbackElement={null} />
</React.StrictMode>
);
}
Loading