Skip to content

Commit 3c187d2

Browse files
committed
Merge branch 'main' into release-next
2 parents 0fe5d6d + ff67b74 commit 3c187d2

File tree

10 files changed

+257
-8
lines changed

10 files changed

+257
-8
lines changed

.github/ISSUE_TEMPLATE/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ contact_links:
55
about: If you've got an idea for a new feature in React Router, please open a new Discussion with the `Proposals` label
66
- name: 🤔 Usage Question (Github Discussions)
77
url: https://github.com/remix-run/remix/discussions/new?category=q-a
8-
about: Open a Discussion in GitHub wih the `Q&A` label
8+
about: Open a Discussion in GitHub with the `Q&A` label
99
- name: 💬 Remix Discord Channel
1010
url: https://rmx.as/discord
1111
about: Interact with other people using React Router and Remix 📀

SECURITY.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,14 @@ To report a security issue, please use the GitHub Security Advisory [Report a Vu
1818

1919
The React Router team will send a response indicating the next steps in handling your report. After the initial reply to your report, our team will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance.
2020

21+
Generally, the full process will look something like this when we receive a new advisory via Github:
22+
23+
- If the advisory is valid, we'll move it into `Draft` status as we begin our investigation
24+
- We'll inform common hosting platforms of the vulnerability so they can make any preventative changes on their end even before the vulnerability is fixed/published
25+
- If you are a hosting provider and you want to be notified right away, please email us at [[email protected]](mailto:[email protected]) and we'll get you added
26+
- We'll publish a new version of React Router with a fix
27+
- We'll update our own sites with the new version
28+
- After a period of time, potentially up to a month or so, we'll publish the advisory
29+
- This gives application developers time to update their applications to the latest version before we make the details of the advisory public
30+
2131
Report security bugs in third-party modules to the person or team maintaining the module. You can also report a vulnerability through the [npm contact form](https://www.npmjs.com/support) by selecting "I'm reporting a security vulnerability".

contributors.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@
195195
- kylegirard
196196
- landisdesign
197197
- latin-1
198+
- lazybean
198199
- lequangdongg
199200
- liborgabrhel
200201
- liuhanqu

decisions/0003-infer-types-for-useloaderdata-and-useactiondata-from-loader-and-action-via-generics.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export default function Route() {
3232
}
3333
```
3434

35-
For end-to-end type safety, it is then the user's responsability to make sure that `loader` and `action` also use the same type in the `json` generic:
35+
For end-to-end type safety, it is then the user's responsibility to make sure that `loader` and `action` also use the same type in the `json` generic:
3636

3737
```ts
3838
export const loader: LoaderFunction = () => {
@@ -108,7 +108,7 @@ export const loader: LoaderFunction = () => {
108108

109109
export default function Route() {
110110
const { birthday } = useLoaderData<MyLoaderData>();
111-
// ^ `useLoaderData` tricks Typescript into thinking this is a `Date`, when in fact its a `string`!
111+
// ^ `useLoaderData` tricks Typescript into thinking this is a `Date`, when in fact it's a `string`!
112112
}
113113
```
114114

@@ -144,7 +144,7 @@ Though `loader` and `useLoaderData` exist together in the same file at developme
144144
Without the `loader` argument to infer types from, `useLoaderData` needs a way to learn about `loader`'s type at compile-time.
145145

146146
Additionally, `loader` and `useLoaderData` are both managed by Remix across the network.
147-
While its true that Remix doesn't "own" the network in the strictest sense, having `useLoaderData` return data that does not correspond to its `loader` is an exceedingly rare edge-case.
147+
While it's true that Remix doesn't "own" the network in the strictest sense, having `useLoaderData` return data that does not correspond to its `loader` is an exceedingly rare edge-case.
148148

149149
Same goes for `useActionData`.
150150

docs/how-to/navigation-blocking.md

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
---
2+
title: Navigation Blocking
3+
---
4+
5+
# Navigation Blocking
6+
7+
[MODES: framework, data]
8+
9+
## Overview
10+
11+
When users are in the middle of a workflow, like filling out an important form, you may want to prevent them from navigating away from the page.
12+
13+
This example will show:
14+
15+
- Setting up a route with a form and action called with a fetcher
16+
- Blocking navigation when the form is dirty
17+
- Showing a confirmation when the user tries to leave the page
18+
19+
## 1. Set up a route with a form
20+
21+
Add a route with the form, we'll use a "contact" route for this example:
22+
23+
```ts filename=routes.ts
24+
import {
25+
type RouteConfig,
26+
index,
27+
route,
28+
} from "@react-router/dev/routes";
29+
30+
export default [
31+
index("routes/home.tsx"),
32+
route("contact", "routes/contact.tsx"),
33+
] satisfies RouteConfig;
34+
```
35+
36+
Add the form to the contact route module:
37+
38+
```tsx filename=routes/contact.tsx
39+
import { useFetcher } from "react-router";
40+
import type { Route } from "./+types/contact";
41+
42+
export async function action({
43+
request,
44+
}: Route.ActionArgs) {
45+
let formData = await request.formData();
46+
let email = formData.get("email");
47+
let message = formData.get("message");
48+
console.log(email, message);
49+
return { ok: true };
50+
}
51+
52+
export default function Contact() {
53+
let fetcher = useFetcher();
54+
55+
return (
56+
<fetcher.Form method="post">
57+
<p>
58+
<label>
59+
Email: <input name="email" type="email" />
60+
</label>
61+
</p>
62+
<p>
63+
<textarea name="message" />
64+
</p>
65+
<p>
66+
<button type="submit">
67+
{fetcher.state === "idle" ? "Send" : "Sending..."}
68+
</button>
69+
</p>
70+
</fetcher.Form>
71+
);
72+
}
73+
```
74+
75+
## 2. Add dirty state and onChange handler
76+
77+
To track the dirty state of the form, we'll use a single boolean and a quick form onChange handler. You may want to track the dirty state differently but this works for this guide.
78+
79+
```tsx filename=routes/contact.tsx lines=[2,8-12]
80+
export default function Contact() {
81+
let [isDirty, setIsDirty] = useState(false);
82+
let fetcher = useFetcher();
83+
84+
return (
85+
<fetcher.Form
86+
method="post"
87+
onChange={(event) => {
88+
let email = event.currentTarget.email.value;
89+
let message = event.currentTarget.message.value;
90+
setIsDirty(Boolean(email || message));
91+
}}
92+
>
93+
{/* existing code */}
94+
</fetcher.Form>
95+
);
96+
}
97+
```
98+
99+
## 3. Block navigation when the form is dirty
100+
101+
```tsx filename=routes/contact.tsx lines=[1,6-8]
102+
import { useBlocker } from "react-router";
103+
104+
export default function Contact() {
105+
let [isDirty, setIsDirty] = useState(false);
106+
let fetcher = useFetcher();
107+
let blocker = useBlocker(
108+
useCallback(() => isDirty, [isDirty])
109+
);
110+
111+
// ... existing code
112+
}
113+
```
114+
115+
While this will now block a navigation, there's no way for the user to confirm it.
116+
117+
## 4. Show confirmation UI
118+
119+
This uses a simple div, but you may want to use a modal dialog.
120+
121+
```tsx filename=routes/contact.tsx lines=[19-41]
122+
export default function Contact() {
123+
let [isDirty, setIsDirty] = useState(false);
124+
let fetcher = useFetcher();
125+
let blocker = useBlocker(
126+
useCallback(() => isDirty, [isDirty])
127+
);
128+
129+
return (
130+
<fetcher.Form
131+
method="post"
132+
onChange={(event) => {
133+
let email = event.currentTarget.email.value;
134+
let message = event.currentTarget.message.value;
135+
setIsDirty(Boolean(email || message));
136+
}}
137+
>
138+
{/* existing code */}
139+
140+
{blocker.state === "blocked" && (
141+
<div>
142+
<p>Wait! You didn't send the message yet:</p>
143+
<p>
144+
<button
145+
type="button"
146+
onClick={() => blocker.proceed()}
147+
>
148+
Leave
149+
</button>{" "}
150+
<button
151+
type="button"
152+
onClick={() => blocker.reset()}
153+
>
154+
Stay here
155+
</button>
156+
</p>
157+
</div>
158+
)}
159+
</fetcher.Form>
160+
);
161+
}
162+
```
163+
164+
If the user clicks "leave" then `blocker.proceed()` will proceed with the navigation. If they click "stay here" then `blocker.reset()` will clear the blocker and keep them on the current page.
165+
166+
## 5. Reset the blocker when the action resolves
167+
168+
If the user doesn't click either "leave" or "stay here", then then submits the form, the blocker will still be active. Let's reset the blocker when the action resolves with an effect.
169+
170+
```tsx filename=routes/contact.tsx
171+
useEffect(() => {
172+
if (fetcher.data?.ok) {
173+
if (blocker.state === "blocked") {
174+
blocker.reset();
175+
}
176+
}
177+
}, [fetcher.data]);
178+
```
179+
180+
## 6. Clear the form when the action resolves
181+
182+
While unrelated to navigation blocking, let's clear the form when the action resolves with a ref.
183+
184+
```tsx
185+
let formRef = useRef<HTMLFormElement>(null);
186+
187+
// put it on the form
188+
<fetcher.Form
189+
ref={formRef}
190+
method="post"
191+
onChange={(event) => {
192+
// ... existing code
193+
}}
194+
>
195+
{/* existing code */}
196+
</fetcher.Form>;
197+
```
198+
199+
```tsx
200+
useEffect(() => {
201+
if (fetcher.data?.ok) {
202+
// clear the form in the effect
203+
formRef.current?.reset();
204+
if (blocker.state === "blocked") {
205+
blocker.reset();
206+
}
207+
}
208+
}, [fetcher.data]);
209+
```
210+
211+
Alternatively, if a navigation is currently blocked, instead of resetting the blocker, you can proceed through to the blocked navigation.
212+
213+
```tsx
214+
useEffect(() => {
215+
if (fetcher.data?.ok) {
216+
if (blocker.state === "blocked") {
217+
// proceed with the blocked navigation
218+
blocker.proceed();
219+
} else {
220+
formRef.current?.reset();
221+
}
222+
}
223+
}, [fetcher.data]);
224+
```
225+
226+
In this case the user flow is:
227+
228+
- User fills out the form
229+
- User forgets to click "send" and clicks a link instead
230+
- The navigation is blocked, and the confirmation message is shown
231+
- Instead of clicking "leave" or "stay here", the user submits the form
232+
- The user is taken to the requested page

docs/start/data/routing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ createBrowserRouter([
5656

5757
## Route Objects
5858

59-
Route objects define the behavior of a route beyond just the path and component, like data loading and actions. We'll go into more detail in the [Route Object guide](./route-objects), but here's a quick example of a loader.
59+
Route objects define the behavior of a route beyond just the path and component, like data loading and actions. We'll go into more detail in the [Route Object guide](./route-object), but here's a quick example of a loader.
6060

6161
```tsx filename=app/team.tsx
6262
import {

integration/fetcher-layout-test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ test("fetcher calls index route loader when at index route", async ({
231231
expect(dataElement.text()).toBe("index data");
232232
});
233233

234-
test("fetcher calls layout route action when at paramaterized route", async ({
234+
test("fetcher calls layout route action when at parameterized route", async ({
235235
page,
236236
}) => {
237237
let app = new PlaywrightFixture(appFixture, page);

integration/helpers/vite-plugin-cloudflare-template/worker-configuration.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5135,7 +5135,7 @@ declare module "assets:*" {
51355135
// https://opensource.org/licenses/Apache-2.0
51365136
declare abstract class PipelineTransform {
51375137
/**
5138-
* transformJson recieves an array of javascript objects which can be
5138+
* transformJson receives an array of javascript objects which can be
51395139
* mutated and returned to the pipeline
51405140
* @param data The data to be mutated
51415141
* @returns A promise containing the mutated data

playground/vite-plugin-cloudflare/worker-configuration.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4272,7 +4272,7 @@ declare module "assets:*" {
42724272
// https://opensource.org/licenses/Apache-2.0
42734273
declare abstract class PipelineTransform {
42744274
/**
4275-
* transformJson recieves an array of javascript objects which can be
4275+
* transformJson receives an array of javascript objects which can be
42764276
* mutated and returned to the pipeline
42774277
* @param data The data to be mutated
42784278
* @returns A promise containing the mutated data

scripts/start-prerelease.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ git checkout dev
1414
git pull
1515
git checkout -b release-next
1616
git merge main --no-edit
17+
18+
if [[ -n $(git status --porcelain) ]]; then
19+
echo "Error: Your git working directory is not clean after merging main ito release-next."
20+
exit 1
21+
fi
22+
1723
pnpm changeset pre enter pre
1824
git add .changeset/pre.json
1925
git commit -m "Enter prerelease mode"

0 commit comments

Comments
 (0)