Skip to content

Commit cedaa3f

Browse files
committed
Merge branch 'release-next'
2 parents 2b12d33 + eade1de commit cedaa3f

File tree

73 files changed

+2761
-834
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

73 files changed

+2761
-834
lines changed

CHANGELOG.md

Lines changed: 135 additions & 98 deletions
Large diffs are not rendered by default.

contributors.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
- BDomzalski
5353
- bhbs
5454
- bilalk711
55+
- bjohn465
5556
- bmsuseluda
5657
- bobziroll
5758
- bravo-kernel
@@ -274,6 +275,7 @@
274275
- mtendekuyokwa19
275276
- mtliendo
276277
- namoscato
278+
- nanianlisao
277279
- ned-park
278280
- nenene3
279281
- ngbrown

docs/api/data-routers/RouterProvider.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ implementation. You _almost always_ want to use the version from
4848
function RouterProvider({
4949
router,
5050
flushSync: reactDomFlushSyncImpl,
51+
unstable_onError,
5152
}: RouterProviderProps): React.ReactElement
5253
```
5354

@@ -63,6 +64,24 @@ You usually don't have to worry about this:
6364
- If you are rendering in a non-DOM environment, you can import
6465
`RouterProvider` from `react-router` and ignore this prop
6566

67+
### unstable_onError
68+
69+
An error handler function that will be called for any loader/action/render
70+
errors that are encountered in your application. This is useful for
71+
logging or reporting errors instead of the `ErrorBoundary` because it's not
72+
subject to re-rendering and will only run one time per error.
73+
74+
The `errorInfo` parameter is passed along from
75+
[`componentDidCatch`](https://react.dev/reference/react/Component#componentdidcatch)
76+
and is only present for render errors.
77+
78+
```tsx
79+
<RouterProvider unstable_onError=(error, errorInfo) => {
80+
console.error(error, errorInfo);
81+
reportToErrorService(error, errorInfo);
82+
}} />
83+
```
84+
6685
### router
6786

6887
The [`DataRouter`](https://api.reactrouter.com/v7/interfaces/react_router.DataRouter.html) instance to use for navigation and data fetching.

docs/api/framework-routers/HydratedRouter.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,21 @@ available to
3838
[`clientAction`](../../start/framework/route-module#clientAction)/[`clientLoader`](../../start/framework/route-module#clientLoader)
3939
functions
4040

41+
### unstable_onError
42+
43+
An error handler function that will be called for any loader/action/render
44+
errors that are encountered in your application. This is useful for
45+
logging or reporting errors instead of the `ErrorBoundary` because it's not
46+
subject to re-rendering and will only run one time per error.
47+
48+
The `errorInfo` parameter is passed along from
49+
[`componentDidCatch`](https://react.dev/reference/react/Component#componentdidcatch)
50+
and is only present for render errors.
51+
52+
```tsx
53+
<HydratedRouter unstable_onError={(error, errorInfo) => {
54+
console.error(error, errorInfo);
55+
reportToErrorService(error, errorInfo);
56+
}} />
57+
```
58+

docs/how-to/middleware.md

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -133,17 +133,9 @@ function getLoadContext(req, res) {
133133

134134
## Quick Start (Data Mode)
135135

136-
### 1. Enable the middleware flag
136+
<docs-info>Note there is no future flag in Data Mode because you can opt-into middleware by adding it to your routes, no breaking changes exist that require a future flag.</docs-info>
137137

138-
```tsx
139-
const router = createBrowserRouter(routes, {
140-
future: {
141-
unstable_middleware: true,
142-
},
143-
});
144-
```
145-
146-
### 2. Create a context
138+
### 1. Create a context
147139

148140
Middleware uses a `context` provider instance to provide data down the middleware chain.
149141
You can create type-safe context objects using [`unstable_createContext`][createContext]:
@@ -156,7 +148,7 @@ export const userContext =
156148
unstable_createContext<User | null>(null);
157149
```
158150

159-
### 3. Add middleware to your routes
151+
### 2. Add middleware to your routes
160152

161153
```tsx
162154
import { redirect } from "react-router";
@@ -216,17 +208,14 @@ export default function Profile() {
216208
}
217209
```
218210

219-
### 4. Add an `unstable_getContext` function (optional)
211+
### 3. Add an `unstable_getContext` function (optional)
220212

221213
If you wish to include a base context on all navigations/fetches, you can add an [`unstable_getContext`][getContext] function to your router. This will be called to populate a fresh context on every navigation/fetch.
222214

223215
```tsx
224216
let sessionContext = unstable_createContext();
225217

226218
const router = createBrowserRouter(routes, {
227-
future: {
228-
unstable_middleware: true,
229-
},
230219
unstable_getContext() {
231220
let context = new unstable_RouterContextProvider();
232221
context.set(sessionContext, getSession());
@@ -256,14 +245,13 @@ export const unstable_middleware: Route.unstable_MiddlewareFunction[] =
256245
[serverMiddleware];
257246
```
258247

259-
Client middleware runs in the browser in framework and data mode for client-side navigations and fetcher calls. Client middleware is different because there's no HTTP [`Request`][request], so it doesn't bubble up anything via the `next` function:
248+
Client middleware runs in the browser in framework and data mode for client-side navigations and fetcher calls. Client middleware differs from server middleware because there's no HTTP Request, so it doesn't have a `Response` to bubble up. In most cases, you can just ignore the return value from `next` and return nothing from your middleware on the client:
260249

261250
```ts
262251
async function clientMiddleware({ request }, next) {
263252
console.log(request.method, request.url);
264-
await next(); // 👈 No return value
253+
await next();
265254
console.log(response.status, request.method, request.url);
266-
// 👈 No need to return anything here
267255
}
268256

269257
// Framework mode
@@ -279,6 +267,34 @@ const route = {
279267
};
280268
```
281269

270+
There may be _some_ cases where you want to do some post-processing based on the result of the loaders/action. In lieu of a `Response`, client middleware bubbles up the value returned from the active [`dataStrategy`][datastrategy] (`Record<string, DataStrategyResult>` - keyed by route id). This allows you to take conditional action in your middleware based on the outcome of the executed `loader`/`action` functions.
271+
272+
Here's an example of the [CMS Redirect on 404][cms-redirect] use case implemented as a client side middleware:
273+
274+
```tsx
275+
async function cmsFallbackMiddleware({ request }, next) {
276+
const results = await next();
277+
278+
// Check if we got a 404 from any of our routes and if so, look for a
279+
// redirect in our CMS
280+
const found404 = Object.values(results).some(
281+
(r) =>
282+
isRouteErrorResponse(r.result) &&
283+
r.result.status === 404,
284+
);
285+
if (found404) {
286+
const cmsRedirect = await checkCMSRedirects(
287+
request.url,
288+
);
289+
if (cmsRedirect) {
290+
throw redirect(cmsRedirect, 302);
291+
}
292+
}
293+
}
294+
```
295+
296+
<docs-warning>In a server middleware, you shouldn't be messing with the `Response` body and should only be reading status/headers and setting headers. Similarly, this value should be considered read-only in client middleware because it represents the "body" or "data" for the resulting navigation which should be driven by loaders/actions - not middleware. This also means that in client middleware, there's usually no need to return the results even if you needed to capture it from `await next()`;</docs-warning>
297+
282298
### When Middleware Runs
283299

284300
It is very important to understand _when_ your middlewares will run to make sure your application is behaving as you intend.
@@ -606,7 +622,7 @@ export const loggingMiddleware = async (
606622
};
607623
```
608624

609-
### 404 to CMS Redirect
625+
### CMS Redirect on 404
610626

611627
```tsx filename=app/middleware/cms-fallback.ts
612628
export const cmsFallbackMiddleware = async (
@@ -703,6 +719,8 @@ export async function loader({
703719
[framework-action]: ../start/framework/route-module#action
704720
[framework-loader]: ../start/framework/route-module#loader
705721
[getloadcontext]: #changes-to-getloadcontextapploadcontext
722+
[datastrategy]: ../api/data-routers/createBrowserRouter#optsdatastrategy
723+
[cms-redirect]: #cms-redirect-on-404
706724
[createContext]: ../api/utils/createContext
707725
[RouterContextProvider]: ../api/utils/RouterContextProvider
708726
[getContext]: ../api/data-routers/createBrowserRouter#optsunstable_getContext
@@ -716,5 +734,3 @@ export async function loader({
716734
[bun]: https://bun.sh/blog/bun-v0.7.0#asynclocalstorage-support
717735
[deno]: https://docs.deno.com/api/node/async_hooks/~/AsyncLocalStorage
718736
[ErrorBoundary]: ../start/framework/route-module#errorboundary
719-
[ErrorResponse]: https://api.reactrouter.com/v7/types/react_router.ErrorResponse.html
720-
[data]: ../api/utils/data

integration/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Minor Changes
66

77
- Unstable Vite support for Node-based Remix apps ([#7590](https://github.com/remix-run/remix/pull/7590))
8+
89
- `remix build` 👉 `vite build && vite build --ssr`
910
- `remix dev` 👉 `vite dev`
1011

integration/browser-entry-test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,70 @@ test("allows users to pass a client side context to HydratedRouter", async ({
129129

130130
appFixture.close();
131131
});
132+
133+
test("allows users to pass an onError function to HydratedRouter", async ({
134+
page,
135+
browserName,
136+
}) => {
137+
let fixture = await createFixture({
138+
files: {
139+
"app/entry.client.tsx": js`
140+
import { HydratedRouter } from "react-router/dom";
141+
import { startTransition, StrictMode } from "react";
142+
import { hydrateRoot } from "react-dom/client";
143+
144+
startTransition(() => {
145+
hydrateRoot(
146+
document,
147+
<StrictMode>
148+
<HydratedRouter
149+
unstable_onError={(error, errorInfo) => {
150+
console.log(error.message, JSON.stringify(errorInfo))
151+
}}
152+
/>
153+
</StrictMode>
154+
);
155+
});
156+
`,
157+
"app/routes/_index.tsx": js`
158+
import { Link } from "react-router";
159+
export default function Index() {
160+
return <Link to="/page">Go to Page</Link>;
161+
}
162+
`,
163+
"app/routes/page.tsx": js`
164+
export default function Page() {
165+
throw new Error("Render error");
166+
}
167+
export function ErrorBoundary({ error }) {
168+
return <h1 data-error>Error: {error.message}</h1>
169+
}
170+
`,
171+
},
172+
});
173+
174+
let logs: string[] = [];
175+
page.on("console", (msg) => logs.push(msg.text()));
176+
177+
let appFixture = await createAppFixture(fixture);
178+
let app = new PlaywrightFixture(appFixture, page);
179+
180+
await app.goto("/", true);
181+
await page.click('a[href="/page"]');
182+
await page.waitForSelector("[data-error]");
183+
184+
expect(await app.getHtml()).toContain("Error: Render error");
185+
expect(logs.length).toBe(2);
186+
// First one is react logging the error
187+
if (browserName === "firefox") {
188+
expect(logs[0]).toContain("Error");
189+
} else {
190+
expect(logs[0]).toContain("Error: Render error");
191+
}
192+
expect(logs[0]).not.toContain("componentStack");
193+
// Second one is ours
194+
expect(logs[1]).toContain("Render error");
195+
expect(logs[1]).toContain('"componentStack":');
196+
197+
appFixture.close();
198+
});

0 commit comments

Comments
 (0)