diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml deleted file mode 100644 index f9e96bf770..0000000000 --- a/.github/workflows/website.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: website - -on: - push: - branches: [main] - paths: - - docs/** - - examples/README.md - - examples/**/README.md - release: - types: [published] - -jobs: - website: - if: github.repository == 'remix-run/react-router' - runs-on: ubuntu-latest - - steps: - - name: Refresh the docs - uses: fjogeleit/http-request-action@v1.8.1 - with: - url: "${{ secrets.DOCS_REFRESH_URL }}?ref=${{ github.ref }}" - method: "POST" - customHeaders: '{"Authorization": "${{ secrets.DOCS_REFRESH_TOKEN }}"}' - - - name: Refresh the staging docs - uses: fjogeleit/http-request-action@v1.8.1 - with: - url: "${{ secrets.DOCS_STAGING_REFRESH_URL }}?ref=${{ github.ref }}" - method: "POST" - customHeaders: '{"Authorization": "${{ secrets.DOCS_REFRESH_TOKEN }}"}' diff --git a/contributors.yml b/contributors.yml index ce76119c45..527ecfea7a 100644 --- a/contributors.yml +++ b/contributors.yml @@ -1,5 +1,6 @@ - abhi-kr-2100 - Ajayff4 +- alexlbr - avipatel97 - awreese - bhbs @@ -54,6 +55,7 @@ - shihanng - shivamsinghchahar - theostavrides +- tanayv - thisiskartik - ThornWu - timdorr @@ -61,3 +63,4 @@ - turansky - underager - vijaypushkin +- vikingviolinist diff --git a/docs/components/form.md b/docs/components/form.md new file mode 100644 index 0000000000..ca40892ebb --- /dev/null +++ b/docs/components/form.md @@ -0,0 +1,307 @@ +--- +title: Form +new: true +--- + +# `
` + +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). + +```tsx +import { Form } from "react-router-dom"; + +function NewEvent() { + return ( + + + + +
+ ); +} +``` + +Make sure your inputs have names or else the `FormData` will not include that field's value. + +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. + +If the form doesn't _feel_ like navigation, you probably want [`useFetcher`][usefetcher]. + +## `action` + +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 `
`, it defaults to the relative URL of the closest route in context. + +Consider the following routes and components: + +```jsx +function ProjectsLayout() { + return ( + <> + + + + ); +} + +function ProjectsPage() { + return ; +} + + + } + action={ProjectsLayout.action} + > + } + action={ProjectsPage.action} + /> + +; +``` + +If the the current URL is `"/projects/123"`, the form inside the child +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 `` and plain HTML forms have the same result. + +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. + +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. + +If you need to post to a different route, then add an action prop: + +```tsx + +``` + +**See also:** + +- [Index Search Param][indexsearchparam] (index vs parent route disambiguation) + +## `method` + +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". + +### GET submissions + +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. + +```tsx + + + + +``` + +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 `` as the developer, but instead you let the user supply the query string dynamically. + +Your route loader can access these values most conveniently by creating a new [`URL`][url] from the `request.url` and then load the data. + +```tsx + { + let url = new URL(request.url); + let searchTerm = url.searchParams.get("q"); + return fakeSearchProducts(searchTerm); + }} +/> +``` + +### Mutation Submissions + +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. + +When the user submits the form, React Router will match the `action` to the app's routes and call the `` 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. + +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. + +```tsx +} + loader={async ({ params }) => { + return fakeLoadProject(params.id) + }} + action={async ({ request, params }) => { + switch (request.method) { + case "put": { + let formData = await request.formData(); + let name = formData.get("projectName"); + return fakeUpdateProject(name); + } + case "delete": { + return fakeDeleteProject(params.id); + } + default { + throw new Response("", { status: 405 }) + } + } + }} +/>; + +function Project() { + let project = useLoaderData(); + + return ( + <> +
+ + +
+ +
+ +
+ + ); +} +``` + +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. + +## `replace` + +Instructs the form to replace the current entry in the history stack, instead of pushing the new entry. + +```tsx +
+``` + +The default behavior is conditional on the form `method`: + +- `get` defaults to `false` +- every other method defaults to `true` + +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. + +In other words, this is really only useful for GET submissions and you want to avoid the back button showing the previous results. + +## `reloadDocument` + +Instructs the form to skip React Router and submit the form with the browser's built in behavior. + +```tsx + +``` + +This is recommended over `` so you can get the benefits of default and relative `action`, but otherwise is the same as a plain HTML form. + +Without a framework like [Remix][remix], or your own server handling of posts to routes, this isn't very useful. + +See also: + +- [`useTransition`][usetransition] +- [`useActionData`][useactiondata] +- [`useSubmit`][usesubmit] + +# Examples + +TODO: More examples + +## Large List Filtering + +A common use case for GET submissions is filtering a large list, like ecommerce and travel booking sites. + +```tsx +function FilterForm() { + return ( + + + +
+ Star Rating + + + + + +
+ +
+ Amenities + + +
+ +
+ ); +} +``` + +When the user submits this form, the form will be serialized to the URL with something like this, depending on the user's selections: + +``` +/slc/hotels?sort=price&stars=4&amenities=pool&amenities=exercise +``` + +You can access those values from the `request.url` + +```tsx + { + let url = new URL(request.url); + let sort = url.searchParams.get("sort"); + let stars = url.searchParams.get("stars"); + let amenities = url.searchParams.getAll("amenities"); + return fakeGetHotels({ sort, stars, amenities }); + }} +/> +``` + +**See also:** + +- [useSubmit][usesubmit] + +[usenavigation]: ../hooks/use-navigation +[formdata]: https://developer.mozilla.org/en-US/docs/Web/API/FormData +[usefetcher]: ../hooks/use-fetcher +[htmlform]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form +[htmlformaction]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-action +[htmlform-method]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-method +[urlsearchparams]: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams +[url]: https://developer.mozilla.org/en-US/docs/Web/API/URL +[usesubmit]: ../hooks/use-submit +[requestmethod]: https://developer.mozilla.org/en-US/docs/Web/API/Request/method +[remix]: https://remix.run +[formvalidation]: https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation +[indexsearchparam]: ../guides/index-search-param diff --git a/docs/components/index.md b/docs/components/index.md index 2499554bbb..ee3eba1746 100644 --- a/docs/components/index.md +++ b/docs/components/index.md @@ -1,4 +1,4 @@ --- title: Components -order: 4 +order: 3 --- diff --git a/docs/components/link-native.md b/docs/components/link-native.md index 5020c5a7d6..669a84ed83 100644 --- a/docs/components/link-native.md +++ b/docs/components/link-native.md @@ -1,5 +1,5 @@ --- -title: Link (React Native) +title: Link (RN) --- # `` (React Native) diff --git a/docs/components/link.md b/docs/components/link.md index edad76f64c..5e7f0f7f00 100644 --- a/docs/components/link.md +++ b/docs/components/link.md @@ -63,3 +63,42 @@ A relative `` value (that does not begin with `/`) resolves relative to > differently when the current URL ends with `/` vs when it does not. [link-native]: ./link-native + +## `resetScroll` + +If you are using [``][scrollrestoration], this lets you prevent the scroll position from being reset to the top of the window when the link is clicked. + +```tsx + +``` + +This does not prevent the scroll position from being restored when the user comes back to the location with the back/forward buttons, it just prevents the reset when the user clicks the link. + +An example when you might want this behavior is a list of tabs that manipulate the url search params that aren't at the top of the page. You wouldn't want the scroll position to jump up to the top because it might scroll the toggled content out of the viewport! + +``` + ┌─────────────────────────┐ + │ ├──┐ + │ │ │ + │ │ │ scrolled + │ │ │ out of view + │ │ │ + │ │ ◄┘ + ┌─┴─────────────────────────┴─┐ + │ ├─┐ + │ │ │ viewport + │ ┌─────────────────────┐ │ │ + │ │ tab tab tab │ │ │ + │ ├─────────────────────┤ │ │ + │ │ │ │ │ + │ │ │ │ │ + │ │ content │ │ │ + │ │ │ │ │ + │ │ │ │ │ + │ └─────────────────────┘ │ │ + │ │◄┘ + └─────────────────────────────┘ + +``` + +[scrollrestoration]: ./scroll-restoration diff --git a/docs/components/route.md b/docs/components/route.md index f083dbe900..e3542cd749 100644 --- a/docs/components/route.md +++ b/docs/components/route.md @@ -2,68 +2,20 @@ title: Route --- -# `` and `` +# Route APIs -
- Type declaration +Because the API and use cases for `` includes data loading, mutations, and more, `` has its own documentation category. -```tsx -declare function Routes( - props: RoutesProps -): React.ReactElement | null; +Please refer to: -interface RoutesProps { - children?: React.ReactNode; - location?: Partial | string; -} +- [``][route] +- [`loader`][loader] +- [`action`][action] +- [`errorElement`][errorelement] +- [`shouldRevalidate`][shouldrevalidate] -declare function Route( - props: RouteProps -): React.ReactElement | null; - -interface RouteProps { - caseSensitive?: boolean; - children?: React.ReactNode; - element?: React.ReactNode | null; - index?: boolean; - path?: string; -} -``` - -
- -`` and `` are the primary ways to render something in React Router based on the current [`location`][location]. You can think about a `` kind of like an `if` statement; if its `path` matches the current URL, it renders its `element`! The `` prop determines if the matching should be done in a case-sensitive manner (defaults to `false`). - -Whenever the location changes, `` looks through all its `children` `` elements to find the best match and renders that branch of the UI. `` elements may be nested to indicate nested UI, which also correspond to nested URL paths. Parent routes render their child routes by rendering an [``][outlet]. - -```tsx - - }> - } - /> - } /> - - } /> - -``` - -> **Note:** -> -> If you'd prefer to define your routes as regular JavaScript objects instead -> of using JSX, [try `useRoutes` instead][use-routes]. - -The default `` is an [``][outlet]. This means the route will still render its children even without an explicit `element` prop, so you can nest route paths without nesting UI around the child route elements. - -For example, in the following config the parent route renders an `` by default, so the child route will render without any surrounding UI. But the child route's path is `/users/:id` because it still builds on its parent. - -```tsx - - } /> - -``` - -[location]: ../hooks/location -[outlet]: ./outlet -[use-route]: ../hooks/use-routes +[route]: ../route/route +[loader]: ../route/loader +[action]: ../route/action +[errorelement]: ../route/error-element +[shouldrevalidate]: ../route/should-revalidate diff --git a/docs/components/routes.md b/docs/components/routes.md index 7f7df768c0..d9f660e682 100644 --- a/docs/components/routes.md +++ b/docs/components/routes.md @@ -2,39 +2,24 @@ title: Routes --- -# `` and `` +# `` -
- Type declaration +Rendered anywhere in the app, `` will match a set of child routes from the current [location][location]. ```tsx -declare function Routes( - props: RoutesProps -): React.ReactElement | null; - interface RoutesProps { children?: React.ReactNode; location?: Partial | string; } -declare function Route( - props: RouteProps -): React.ReactElement | null; - -interface RouteProps { - caseSensitive?: boolean; - children?: React.ReactNode; - element?: React.ReactNode | null; - index?: boolean; - path?: string; -} + + +; ``` -
- -`` and `` are the primary ways to render something in React Router based on the current [`location`][location]. You can think about a `` kind of like an `if` statement; if its `path` matches the current URL, it renders its `element`! The `` prop determines if the matching should be done in a case-sensitive manner (defaults to `false`). +Note that if you're using a data router like [``][databrowserrouter] it is uncommon to use this component as it does not participate in data loading. -Whenever the location changes, `` looks through all its `children` `` elements to find the best match and renders that branch of the UI. `` elements may be nested to indicate nested UI, which also correspond to nested URL paths. Parent routes render their child routes by rendering an [``][outlet]. +Whenever the location changes, `` looks through all its child routes to find the best match and renders that branch of the UI. `` elements may be nested to indicate nested UI, which also correspond to nested URL paths. Parent routes render their child routes by rendering an [``][outlet]. ```tsx @@ -49,21 +34,7 @@ Whenever the location changes, `` looks through all its `children` ` ``` -> **Note:** -> -> If you'd prefer to define your routes as regular JavaScript objects instead -> of using JSX, [try `useRoutes` instead][use-routes]. - -The default `` is an [``][outlet]. This means the route will still render its children even without an explicit `element` prop, so you can nest route paths without nesting UI around the child route elements. - -For example, in the following config the parent route renders an `` by default, so the child route will render without any surrounding UI. But the child route's path is `/users/:id` because it still builds on its parent. - -```tsx - - } /> - -``` - [location]: ../hook/location [outlet]: ./outlet [use-route]: ../hooks/use-routes +[databrowserrouter]: ../routers/data-browser-router diff --git a/docs/components/scroll-restoration.md b/docs/components/scroll-restoration.md new file mode 100644 index 0000000000..ee20e7c3c5 --- /dev/null +++ b/docs/components/scroll-restoration.md @@ -0,0 +1,111 @@ +--- +title: ScrollRestoration +new: true +--- + +# `` + +This component will emulate the browser's scroll restoration on location changes after loaders have completed to ensure the scroll position is restored to the right spot, even across domains. + +You should only render one of these and it's recommended you render it in the root route of your app: + +```tsx +import * as React from "react"; +import * as ReactDOM from "react-dom"; +import { + DataBrowserRouter, + ScrollRestoration, +} from "react-router-dom"; + +function Root() { + return ( +
+ {/* ... */} + +
+ ); +} + +ReactDOM.render( + + }>{/* child routes */} + , + root +); +``` + +## `getKey` + +Optional prop that defines the key React Router should use to restore scroll positions. + +```tsx + { + // default behavior + return location.key; + }} +/> +``` + +By default it uses `location.key`, emulating the browser's default behavior without client side routing. The user can navigate to the same URL multiple times in the stack and each entry gets its own scroll position to restore. + +Some apps may want to override this behavior and restore position based on something else. Consider a social app that has four primary pages: + +- "/home" +- "/messages" +- "/notifications" +- "/search" + +If the user starts at "/home", scrolls down a bit, clicks "messages" in the navigation menu, then clicks "home" in the navigation menu (not the back button!) there will be three entries in the history stack: + +``` +1. /home +2. /messages +3. /home +``` + +By default, React Router (and the browser) will have two different scroll positions stored for `1` and `3` even though they have the same URL. That means as the user navigated from `2` → `3` the scroll position goes to the top instead of restoring to where it was in `1`. + +A solid product decision here is to keep the users scroll position on the home feed no matter how they got there (back button or new link clicks). For this, you'd want to use the `location.pathname` as the key. + +```tsx + { + return location.pathname; + }} +/> +``` + +Or you may want to only use the pathname for some paths, and use the normal behavior for everything else: + +```tsx + { + const paths = ["/home", "/notifications"]; + return paths.includes(location.pathname) + ? // home and notifications restore by pathname + location.pathname + : // everything else by location like the browser + location.key; + }} +/> +``` + +## Preventing Scroll Reset + +When navigation creates new scroll keys, the scroll position is reset to the top of the page. You can prevent the "scroll to top" behavior from your links: + +```tsx + +``` + +See also: [``][resetscroll] + +## Scroll Flashing + +Without a server side rendering framework like [Remix][remix], you may experience some scroll flashing on initial page loads. This is because React Router can't restore scroll position until your JS bundles have downloaded (because React Router doesn't exist yet). It also has to wait for the data to load and the the page to render completely before it can accurately restore scroll (if you're rendering a spinner, the viewport is likely not the size it was when the scroll position was saved). + +With server rendering in Remix, the document comes to the browser fully formed and Remix actually lets the browser restore the scroll position with the browser's own default behavior. + +[remix]: https://remix.run +[resetscroll]: ../components/link#resetscroll diff --git a/docs/fetch/index.md b/docs/fetch/index.md new file mode 100644 index 0000000000..31ad0db371 --- /dev/null +++ b/docs/fetch/index.md @@ -0,0 +1,4 @@ +--- +title: Fetch Utilities +order: 5 +--- diff --git a/docs/fetch/is-route-error-response.md b/docs/fetch/is-route-error-response.md new file mode 100644 index 0000000000..f008b766cf --- /dev/null +++ b/docs/fetch/is-route-error-response.md @@ -0,0 +1,3 @@ +--- +title: isRouteErrorResponse +--- diff --git a/docs/fetch/json.md b/docs/fetch/json.md new file mode 100644 index 0000000000..a649361274 --- /dev/null +++ b/docs/fetch/json.md @@ -0,0 +1,3 @@ +--- +title: json +--- diff --git a/docs/fetch/redirect.md b/docs/fetch/redirect.md new file mode 100644 index 0000000000..8358272f4a --- /dev/null +++ b/docs/fetch/redirect.md @@ -0,0 +1,3 @@ +--- +title: redirect +--- diff --git a/docs/getting-started/concepts.md b/docs/getting-started/concepts.md index 770e12ca6d..28671de1a0 100644 --- a/docs/getting-started/concepts.md +++ b/docs/getting-started/concepts.md @@ -1,6 +1,6 @@ --- title: Main Concepts -order: 4 +order: 5 --- # Main Concepts @@ -171,7 +171,7 @@ The last two, `{ state, key }`, are React Router specific. **Location Pathname** -This is the part of [URL](#url) after the origin, so for `https://example.com/teams/hotspurs` the pathname is `/teams/hostspurs`. This is the only part of the location that routes match against. +This is the part of [URL](#url) after the origin, so for `https://example.com/teams/hotspurs` the pathname is `/teams/hotspurs`. This is the only part of the location that routes match against. **Location Search** @@ -211,7 +211,7 @@ Hashes in URLs indicate a scroll position _on the current page_. Before the `win You may have wondered why the `window.history.pushState()` API is called "push state". State? Aren't we just changing the [URL](#url)? Shouldn't it be `history.push`? Well, we weren't in the room when the API was designed, so we're not sure why "state" was the focus, but it is a cool feature of browsers nonetheless. -Browsers let us persist information about a transition by passing a value to `pushState`. When the user clicks back, the value on `history.state` changes to whatever was "pushed" before. +Browsers let us persist information about a navigation by passing a value to `pushState`. When the user clicks back, the value on `history.state` changes to whatever was "pushed" before. ```js window.history.pushState("look ma!", undefined, "/contact"); diff --git a/docs/getting-started/data.md b/docs/getting-started/data.md new file mode 100644 index 0000000000..49cb25472a --- /dev/null +++ b/docs/getting-started/data.md @@ -0,0 +1,404 @@ +--- +title: Data Quick Start +new: true +order: 3 +--- + +# Data APIs Quick Start + +Follow along with the [completed demo on stackblitz][demo]. + +This guide is intended for people with some experience with React Router v6 already. If you are brand new to React Router, you will probably want to review the normal [Quick Start][quickstart] first. + +React Router v6.4 introduces all of the data abstractions from [Remix][remix] for React Router SPAs. Now you can keep your UI _and your data_ in sync with the URL automatically. + +## Installation + +The new Data APIs are available on the `next` tag: + +```sh +npm install react-router-dom@next +``` + +## Configuring Routes + +Configuring routes is the same, except you need to use [`DataBrowserRouter`][databrowserrouter] to enable the data APIs. note you no longer render `` either, just ``. + +It's important to render `DataBrowserRouter` at the top of the React tree. + +```jsx filename=main.jsx [4,8-10] +import "./index.css"; +import React from "react"; +import { createRoot } from "react-dom/client"; +import { DataBrowserRouter, Route } from "react-router-dom"; +import Root from "./routes/root"; + +createRoot(document.getElementById("root")).render( + + } /> + +); +``` + +Let's check out our root route: + +```jsx filename=routes/root.jsx +import { Outlet } from "react-router-dom"; + +export default function Root() { + return ( +
+

Notes!

+ +
+ ); +} +``` + +## Data Loading + +Now that we have a router in place, we can add [loaders][loader] to our routes to provide data to components. + +First we'll export a loader from the root route and then access the data with [`useLoaderData`][useloaderdata]. + +```jsx filename=routes/root.jsx lines=[1,3-11,14] +import { useLoaderData, Link } from "react-router-dom"; + +export async function loader() { + return [ + { + id: "abc", + title: "Fake Note", + content: "We'll replace this with real data soon", + }, + ]; +} + +export default function Root() { + const notes = useLoaderData(); + + return ( +
+ {notes.map((note) => ( +
+ {note.title} +
+ ))} + +
+ ); +} +``` + +Next we add the [loader][loader] to the route config: + +```jsx filename=main.jsx lines=[2,9] +// ... +import Root, { loader as rootLoader } from "./routes/root"; + +createRoot(document.getElementById("root")).render( + + } + loader={rootLoader} + /> + +); +``` + +Feel free to add some styles, we're just getting the data on the page here 😋. + +If you're coding along with this, you'll want to copy paste our data model into `notes.js`. + +
+View data model code + +```sh +npm install localforage +``` + +```jsx filename=notes.js +import localforage from "localforage"; + +export async function getNotes() { + let notes = await localforage.getItem("notes"); + if (!notes) notes = []; + return notes; +} + +export async function createNote({ title, content }) { + let id = Math.random().toString(36).substring(2, 9); + let note = { id, title, content }; + let notes = await getNotes(); + notes.unshift(note); + await set(notes); + return note; +} + +export async function getNote(id) { + let notes = await localforage.getItem("notes"); + let note = notes.find((note) => note.id === id); + return note ?? null; +} + +export async function deleteNote(id) { + let notes = await localforage.getItem("notes"); + let index = notes.findIndex((note) => note.id === id); + if (index > -1) { + notes.splice(index, 1); + await set(notes); + return true; + } + return false; +} + +function set(notes) { + return localforage.setItem("notes", notes); +} +``` + +
+ +With our real data in place, we can change our root [loader][loader] to use it: + +```jsx lines=[2,5] filename=routes/root.jsx +import { useLoaderData, Link } from "react-router-dom"; +import { getNotes } from "../notes"; + +export async function loader() { + return getNotes(); +} + +export default function Root() { + // ... +} +``` + +But now we have an empty page. It's time for a form. + +## Data Mutations + +In order to create new notes, we'll create a form and add it to a route at "/new". + +```jsx filename=routes/new.jsx +import { Form } from "react-router-dom"; + +export default function NewNote() { + return ( +
+

+ +

+

+ +
+