Skip to content

Commit 19d5fa6

Browse files
brophdawg11ryanflorencealexloberatanayvremix-cla-bot[bot]
authored
Add Remix Data APIs (#8937)
* feat: add history package and createMemoryHistory implementation (#8702) * feat: add initial empty history package * feat: add createMemoryHistory + tests * chore: add rollup buld for history * chore: change state type from unknown -> any * feat: Change listen interface to be pop only * chore: fix lint warning * Add history package to yarn workspace * chore: Address PR feedback * feat: Add remix-router package and loader functionality Additional notes: - adds unique route id generation to react-router - enhances test harness to manage at navigation level - combines error/catch into a single exception * feat: add support for submissions * chore: code organization + cleanup * ci: copy over unit test from transition manager * fix: Fix action arguments bug * feat: add createBrowserHistory and createHashHistory * chore: rename package to @remix-run/router * wip: initial router integration with React * wip: add userLoaderData * Remove history package and move history.ts into router * fix: fix URLSearchParams keys typescript error This one was tricky: - react-router-native used to find the type for URLSearchParams from @types/react-native - The new history work added @types/jsdom - react-router-native now finds that URLSearchParams before the @types/react-native one - the jsdom one imports from the internal built-in TS DOM lib - but it does not include the DOM.Iterable values such as keys See: microsoft/TypeScript#38139 * feat: add data loading/rendering APIs - loader/action - useLoaderData/useActionData/useTransition - exceptionElement/useRouteException - fallbackElement * feat: add browser/hash routers and bring over useSubmit * feat: bring <Form> to react router-dom * feat: support redirects returned from loaders * feat: Support basename in data routers * chore: remove backup _components file * chore: extract dom.ts utils file * feat: add <Route shouldReload> and fix bug in shouldReload logic * chore: convert to router.subscribe for easier useSyncExternalStore usage * chore: switch to babel-jest TS transform instead of ts-jest * fix: make route id optional for backwards compatibility * feat: default fallbackElement/exceptionElement + add useMacthes/useRouteData * fix: Fix up build issues with v5-compat * chore: renames and feedback cleanup * feat: handle render errors through exceptionElement * feat: move initial data load into router * chore: do not handle exceptions for non-data-routers * chore: remove historyv5 dependency * feat: add support for router.revalidate() * feat: add useRevalidator() hook * fix: fix shouldReload support for revalidate() * fix: handle 405 action responses correctly This matches the behavior added to Remix in remix-run/remix#2342 * feat: handle X-Remix-Revalidate hesders returned from loader redirects This ports over the remix behavior from remix-run/remix#2370 * chore: PR feedback * chore: copy transition fetcher tests verbatim * feat: add support for fetchers * feat: add useFetcher * ci: add tests for useFetcher/useFetchers * chore: add formAction to submission * chore: remove basename from data routers * chore: change shouldReload url type * chore: change default fallback element * chore: add tests for cleanup and make Router type explicit * feat: support returning responses from loaders/actions automatically parses the response body with json() or text() depending on the content type of the response. This makes it really convenient to use `fetch` and brings the server semantics to client loaders like `json(data)` or `new Response(“”, { status: 404 })` etc. * chore: cleanup auto-unwrap logic, add action info to request, remove action submission fields * feat: provide full control to users in shouldRevalidate * chore: remove hashchange and create createUrlBasedHistory abstraction * fix: handle syncronous initial load * fix request patch * feat: support fetcher opt-in revalidation * ci: clean up revalidating fetcher tests and logic * feat: wiure up useFetcher and turn off revalidation on submit * docs: update transition/fetcher diagrams * ci: bump bundle thresholds for new data routing * chore: update to latest @web-std/fetch with proper formData handling Our PR was merged and released in 4.1.0 so we can remove our local override. See: web-std/io#60 * feat: move to better cross-browser fallback spinner * Revert "feat: move to better cross-browser fallback spinner" This reverts commit a887019. * chore: Rename "exception" to "error" Done in steps via grep/sed: - exceptionElement -> errorElement - useRouteException -> useRouteError - Exception -> Error - exceptions -> errors - exception -> error * feat: handle revalidation for interrupted submissions feat: change defaultShouldRevalidate from funciton -> boolean chore: simplify revalidation via isRevalidationRequired * feat: opt-in fetcher.load to revalidation by default * feat: send actionResult to shouldRevalidate * feat: add <ScrollRestoration> component and <Link resetScroll=false> * chore: rename navigation -> transition * feat: flatten submission onto shouldRevalidate * feat: simplify form submit logic via event.submitter Copies the changes from remix remix-run/remix#3094 * chore: clean up types * chore: sync up react-router-native public api * feat: properly support React 18 + React.StrictMode * chore: fix lint warnings * chore: remove default fallback element * chore: add @remix-run/router to example vite configs for USE_SOURCE * chore: add data-router and scroll-restoration examples * chore: switch from @web-std/fetch to @remix-run/web-fetch for tests * feat: add pending logic to NavLink (#8875) * feat: add pending logic to NavLink * chore: fixup type imports * chore: add useMemo to nextMatch calculation * chore: inline react use-sync-external-store/shim for UMD builds * chore: clean up some exports and add some jsdocs * feat: add DataStaticRouter for SSR * chore: fix link * fix: default replace=false only for GET submissions * chore: update scroll restoration example to include data loading * feat: remove fetcher.type since it can be derived * feat: Remove promise from return signature of router methods * chore: remove encType from Form props * WIP docs * feat: auto-unwrap error Responses into ErrorResponse * chore: add error boundary example * feat: support routes prop on data routers * docs: moar docs * chore: remove navigation.type * docs docs docs * dooooooooooooooooooocs * docs: fix contributing link (#8885) * docs: fix contributing link * chore: add alexlbr to contributors * Docs: Fix typo in getting-started > concepts (#8888) * docs: fix small typo in getting-started > concepts * Sign CLA: add tanayv to contributors.yml * chore: sort contributors list * docs: think it’s ready for the big time * docs: notes demo * Update overview.md (#8892) * Update overview.md * Update contributors.yml * Version 6.4.0-pre.0 * docs: link fixes * chore: publish @remix-run/router * Version 6.4.0-pre.1 * chore: publish @remix-run/router * Version 6.4.0-pre.2 * docs: DataStaticRouter stub * docs: not sure what this was 😂 * docs: link to the demo * fix: properly handle 404s using error boundaries * docs: getting started installation * Convert formMethod=GET to be a loading navigation instead of submitting * Handle invalid GET formData via 400 Bad Request error * fix: Properly handle descendent routes within a data router * docs: update useMatches and useNavigation docs * fix: better solution for 404 error boundaries * chore: refactor resetScroll to avoid history state mutation * docs: add note on useMatches and descendent routes * chore: move newly added resolveTo test from react-router to router * ci: Remove website workflow before merging remixing work into dev Co-authored-by: Ryan Florence <[email protected]> Co-authored-by: Alex Lobera <[email protected]> Co-authored-by: Tanay Vardhan <[email protected]> Co-authored-by: remix-cla-bot[bot] <92060565+remix-cla-bot[bot]@users.noreply.github.com> Co-authored-by: Michal Petrik <[email protected]>
1 parent 9d75bfc commit 19d5fa6

File tree

205 files changed

+29737
-623
lines changed

Some content is hidden

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

205 files changed

+29737
-623
lines changed

Diff for: .github/workflows/website.yml

-31
This file was deleted.

Diff for: contributors.yml

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
- abhi-kr-2100
22
- Ajayff4
3+
- alexlbr
34
- avipatel97
45
- awreese
56
- bhbs
@@ -54,10 +55,12 @@
5455
- shihanng
5556
- shivamsinghchahar
5657
- theostavrides
58+
- tanayv
5759
- thisiskartik
5860
- ThornWu
5961
- timdorr
6062
- tkindy
6163
- turansky
6264
- underager
6365
- vijaypushkin
66+
- vikingviolinist

Diff for: docs/components/form.md

+307
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
---
2+
title: Form
3+
new: true
4+
---
5+
6+
# `<Form>`
7+
8+
The Form component is a wrapper around a plain HTML [form][htmlform] that emulates the browser for client side routing and data mutations. It is _not_ a form validation/state management library like you might be used to in the React ecosystem (for that, we recommend the browser's built in [HTML Form Validation][formvalidation] and data validation on your backend server).
9+
10+
```tsx
11+
import { Form } from "react-router-dom";
12+
13+
function NewEvent() {
14+
return (
15+
<Form method="post" action="/events">
16+
<input type="text" name="title" />
17+
<input type="text" name="description" />
18+
<button type="submit">Create</button>
19+
</Form>
20+
);
21+
}
22+
```
23+
24+
<docs-info>Make sure your inputs have names or else the `FormData` will not include that field's value.</docs-info>
25+
26+
All of this will trigger state updates to any rendered [`useNavigation`][usenavigation] hooks so you can build pending indicators and optimistic UI while the async operations are in-flight.
27+
28+
If the form doesn't _feel_ like navigation, you probably want [`useFetcher`][usefetcher].
29+
30+
## `action`
31+
32+
The url to which the form will be submitted, just like [HTML form action][htmlformaction]. The only difference is the default action. With HTML forms, it defaults to the full URL. With `<Form>`, it defaults to the relative URL of the closest route in context.
33+
34+
Consider the following routes and components:
35+
36+
```jsx
37+
function ProjectsLayout() {
38+
return (
39+
<>
40+
<Form method="post" />
41+
<Outlet />
42+
</>
43+
);
44+
}
45+
46+
function ProjectsPage() {
47+
return <Form method="post" />;
48+
}
49+
50+
<DataBrowserRouter>
51+
<Route
52+
path="/projects"
53+
element={<ProjectsLayout />}
54+
action={ProjectsLayout.action}
55+
>
56+
<Route
57+
path=":projectId"
58+
element={<ProjectPage />}
59+
action={ProjectsPage.action}
60+
/>
61+
</Route>
62+
</DataBrowserRouter>;
63+
```
64+
65+
If the the current URL is `"/projects/123"`, the form inside the child
66+
route, `ProjectsPage`, will have a default action as you might expect: `"/projects/123"`. In this case, where the route is the deepest matching route, both `<Form>` and plain HTML forms have the same result.
67+
68+
But the form inside of `ProjectsLayout` will point to `"/projects"`, not the full URL. In other words, it points to the matching segment of the URL for the route in which the form is rendered.
69+
70+
This helps with portability as well as co-location of forms and their action handlers when if you add some convention around your route modules.
71+
72+
If you need to post to a different route, then add an action prop:
73+
74+
```tsx
75+
<Form action="/projects/new" method="post" />
76+
```
77+
78+
**See also:**
79+
80+
- [Index Search Param][indexsearchparam] (index vs parent route disambiguation)
81+
82+
## `method`
83+
84+
This determines the [HTTP verb](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods) to be used. The same as plain HTML [form method][htmlform-method], except it also supports "put", "patch", and "delete" in addition to "get" and "post". The default is "get".
85+
86+
### GET submissions
87+
88+
The default method is "get". Get submissions _will not call an action_. Get submissions are the same as a normal navigation (user clicks a link) except the user gets to supply the search params that go to the URL from the form.
89+
90+
```tsx
91+
<Form method="get" action="/products">
92+
<input
93+
aria-label="search products"
94+
type="text"
95+
name="q"
96+
/>
97+
<button type="submit">Search</button>
98+
</Form>
99+
```
100+
101+
Let's say the user types in "running shoes" and submits the form. React Router emulates the browser and will serialize the form into [URLSearchParams][urlsearchparams] and then navigate the user to `"/products?q=running+shoes"`. It's as if you rendered a `<Link to="/products?q=running+shoes">` as the developer, but instead you let the user supply the query string dynamically.
102+
103+
Your route loader can access these values most conveniently by creating a new [`URL`][url] from the `request.url` and then load the data.
104+
105+
```tsx
106+
<Route
107+
path="/products"
108+
loader={async ({ request }) => {
109+
let url = new URL(request.url);
110+
let searchTerm = url.searchParams.get("q");
111+
return fakeSearchProducts(searchTerm);
112+
}}
113+
/>
114+
```
115+
116+
### Mutation Submissions
117+
118+
All other methods are "mutation submissions", meaning you intend to change something about your data with POST, PUT, PATCH, or DELETE. Note that plain HTML forms only support "post" and "get", we tend to stick to those two as well.
119+
120+
When the user submits the form, React Router will match the `action` to the app's routes and call the `<Route action>` with the serialized [`FormData`][formdata]. When the action completes, all of the loader data on the page will automatically revalidate to keep your UI in sync with your data.
121+
122+
The method will be available on [`request.method`][requestmethod] inside the route action that is called. You can use this to instruct your data abstractions about the intent of the submission.
123+
124+
```tsx
125+
<Route
126+
path="/projects/:id"
127+
element={<Project />}
128+
loader={async ({ params }) => {
129+
return fakeLoadProject(params.id)
130+
}}
131+
action={async ({ request, params }) => {
132+
switch (request.method) {
133+
case "put": {
134+
let formData = await request.formData();
135+
let name = formData.get("projectName");
136+
return fakeUpdateProject(name);
137+
}
138+
case "delete": {
139+
return fakeDeleteProject(params.id);
140+
}
141+
default {
142+
throw new Response("", { status: 405 })
143+
}
144+
}
145+
}}
146+
/>;
147+
148+
function Project() {
149+
let project = useLoaderData();
150+
151+
return (
152+
<>
153+
<Form method="put">
154+
<input
155+
type="text"
156+
name="projectName"
157+
defaultValue={project.name}
158+
/>
159+
<button type="submit">Update Project</button>
160+
</Form>
161+
162+
<Form method="delete">
163+
<button type="submit">Delete Project</button>
164+
</Form>
165+
</>
166+
);
167+
}
168+
```
169+
170+
As you can see, both forms submit to the same route but you can use the `request.method` to branch on what you intend to do. After the actions completes, the `loader` will be revalidated and the UI will automatically synchronize with the new data.
171+
172+
## `replace`
173+
174+
Instructs the form to replace the current entry in the history stack, instead of pushing the new entry.
175+
176+
```tsx
177+
<Form replace />
178+
```
179+
180+
The default behavior is conditional on the form `method`:
181+
182+
- `get` defaults to `false`
183+
- every other method defaults to `true`
184+
185+
We've found with `get` you often want the user to be able to click "back" to see the previous search results/filters, etc. But with the other methods the default is `true` to avoid the "are you sure you want to resubmit the form?" prompt. Note that even if `replace={false}` React Router _will not_ resubmit the form when the back button is clicked and the method is post, put, patch, or delete.
186+
187+
In other words, this is really only useful for GET submissions and you want to avoid the back button showing the previous results.
188+
189+
## `reloadDocument`
190+
191+
Instructs the form to skip React Router and submit the form with the browser's built in behavior.
192+
193+
```tsx
194+
<Form reloadDocument />
195+
```
196+
197+
This is recommended over `<form>` so you can get the benefits of default and relative `action`, but otherwise is the same as a plain HTML form.
198+
199+
Without a framework like [Remix][remix], or your own server handling of posts to routes, this isn't very useful.
200+
201+
See also:
202+
203+
- [`useTransition`][usetransition]
204+
- [`useActionData`][useactiondata]
205+
- [`useSubmit`][usesubmit]
206+
207+
# Examples
208+
209+
TODO: More examples
210+
211+
## Large List Filtering
212+
213+
A common use case for GET submissions is filtering a large list, like ecommerce and travel booking sites.
214+
215+
```tsx
216+
function FilterForm() {
217+
return (
218+
<Form method="get" action="/slc/hotels">
219+
<select name="sort">
220+
<option value="price">Price</option>
221+
<option value="stars">Stars</option>
222+
<option value="distance">Distance</option>
223+
</select>
224+
225+
<fieldset>
226+
<legend>Star Rating</legend>
227+
<label>
228+
<input type="radio" name="stars" value="5" />{" "}
229+
★★★★★
230+
</label>
231+
<label>
232+
<input type="radio" name="stars" value="4" /> ★★★★
233+
</label>
234+
<label>
235+
<input type="radio" name="stars" value="3" /> ★★★
236+
</label>
237+
<label>
238+
<input type="radio" name="stars" value="2" /> ★★
239+
</label>
240+
<label>
241+
<input type="radio" name="stars" value="1" /> ★
242+
</label>
243+
</fieldset>
244+
245+
<fieldset>
246+
<legend>Amenities</legend>
247+
<label>
248+
<input
249+
type="checkbox"
250+
name="amenities"
251+
value="pool"
252+
/>{" "}
253+
Pool
254+
</label>
255+
<label>
256+
<input
257+
type="checkbox"
258+
name="amenities"
259+
value="exercise"
260+
/>{" "}
261+
Exercise Room
262+
</label>
263+
</fieldset>
264+
<button type="submit">Search</button>
265+
</Form>
266+
);
267+
}
268+
```
269+
270+
When the user submits this form, the form will be serialized to the URL with something like this, depending on the user's selections:
271+
272+
```
273+
/slc/hotels?sort=price&stars=4&amenities=pool&amenities=exercise
274+
```
275+
276+
You can access those values from the `request.url`
277+
278+
```tsx
279+
<Route
280+
path="/:city/hotels"
281+
loader={async ({ request }) => {
282+
let url = new URL(request.url);
283+
let sort = url.searchParams.get("sort");
284+
let stars = url.searchParams.get("stars");
285+
let amenities = url.searchParams.getAll("amenities");
286+
return fakeGetHotels({ sort, stars, amenities });
287+
}}
288+
/>
289+
```
290+
291+
**See also:**
292+
293+
- [useSubmit][usesubmit]
294+
295+
[usenavigation]: ../hooks/use-navigation
296+
[formdata]: https://developer.mozilla.org/en-US/docs/Web/API/FormData
297+
[usefetcher]: ../hooks/use-fetcher
298+
[htmlform]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form
299+
[htmlformaction]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-action
300+
[htmlform-method]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-method
301+
[urlsearchparams]: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams
302+
[url]: https://developer.mozilla.org/en-US/docs/Web/API/URL
303+
[usesubmit]: ../hooks/use-submit
304+
[requestmethod]: https://developer.mozilla.org/en-US/docs/Web/API/Request/method
305+
[remix]: https://remix.run
306+
[formvalidation]: https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation
307+
[indexsearchparam]: ../guides/index-search-param

Diff for: docs/components/index.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
---
22
title: Components
3-
order: 4
3+
order: 3
44
---

Diff for: docs/components/link-native.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
title: Link (React Native)
2+
title: Link (RN)
33
---
44

55
# `<Link>` (React Native)

0 commit comments

Comments
 (0)