diff --git a/docs/start/framework/route-module.md b/docs/start/framework/route-module.md index 531169e0d1..36e504cb7e 100644 --- a/docs/start/framework/route-module.md +++ b/docs/start/framework/route-module.md @@ -137,7 +137,7 @@ See also: ## `clientMiddleware` -This is the client-side equivalent of `middleware` and runs in the browser during client navigations. The only difference from server middleware is that client middleware doesn't return Responses because they're not wrapping an HTTP request on the server. +This is the client-side equivalent of `middleware` and runs in the browser during client navigations. The only difference from server middleware is that client middleware doesn't return Responses because it's not wrapping an HTTP request on the server. Here's an example middleware to log requests on the client: diff --git a/docs/start/framework/testing.md b/docs/start/framework/testing.md index dd2059f020..536f97edf8 100644 --- a/docs/start/framework/testing.md +++ b/docs/start/framework/testing.md @@ -80,7 +80,7 @@ test("LoginForm renders error messages", async () => { ## Using with Framework Mode Types -It's important to note that `createRoutesStub` is designed for _unit_ testing of reusable components in your application that rely on on contextual router information (i.e., `loaderData`, `actionData`, `matches`). These components usually obtain this information via the hooks (`useLoaderData`, `useActionData`, `useMatches`) or via props passed down from the ancestor route component. We **strongly** recommend limiting your usage of `createRoutesStub` to unit testing of these types of reusable components. +It's important to note that `createRoutesStub` is designed for _unit_ testing of reusable components in your application that rely on contextual router information (i.e., `loaderData`, `actionData`, `matches`). These components usually obtain this information via the hooks (`useLoaderData`, `useActionData`, `useMatches`) or via props passed down from the ancestor route component. We **strongly** recommend limiting your usage of `createRoutesStub` to unit testing of these types of reusable components. `createRoutesStub` is _not designed_ for (and is arguably incompatible with) direct testing of Route components using the [`Route.\*`](../../explanation/type-safety) types available in Framework Mode. This is because the `Route.*` types are derived from your actual application - including the real `loader`/`action` functions as well as the structure of your route tree structure (which defines the `matches` type). When you use `createRoutesStub`, you are providing stubbed values for `loaderData`, `actionData`, and even your `matches` based on the route tree you pass to `createRoutesStub`. Therefore, the types won't align with the `Route.*` types and you'll get type issues trying to use a route component in a route stub. diff --git a/docs/tutorials/address-book.md b/docs/tutorials/address-book.md index 3e356eb455..6972c2ce1b 100644 --- a/docs/tutorials/address-book.md +++ b/docs/tutorials/address-book.md @@ -408,12 +408,10 @@ First we'll create and export a [`clientLoader`][client-loader] function in the The following code has a type error in it, we'll fix it in the next section -```tsx filename=app/root.tsx lines=[2,6-9,11-12,19-42] +```tsx filename=app/root.tsx lines=[2,4-7,9-11,17-40] // existing imports import { getContacts } from "./data"; -// existing exports - export async function clientLoader() { const contacts = await getContacts(); return { contacts }; @@ -646,7 +644,7 @@ export default function About() { } ``` -👉 **Add a link to the about page in the sidebar** +👉 **Transform the text inside the h1-tags into a link to the about page in the sidebar** ```tsx filename=app/root.tsx lines=[5-7] export default function App() { @@ -813,7 +811,7 @@ If you refresh the about page, you still see the loading spinner for just a spli Inside of `react-router.config.ts`, we can add a [`prerender`][pre-rendering] array to the config to tell React Router to pre-render certain urls at build time. In this case we just want to pre-render the about page. -```ts filename=app/react-router.config.ts lines=[5] +```ts filename=react-router.config.ts lines=[5] import { type Config } from "@react-router/dev/config"; export default { @@ -838,7 +836,7 @@ If you ever do want to introduce server-side rendering into your React Router ap 👉 **Enable server-side rendering** -```ts filename=app/react-router.config.ts lines=[2] +```ts filename=react-router.config.ts lines=[2] export default { ssr: true, prerender: ["/about"], @@ -862,7 +860,7 @@ export async function loader() { } ``` -Whether you set `ssr` to `true` or `false` depends on you and your users needs. Both strategies are perfectly valid. For the remainder of this tutorial we're going to use server-side rendering, but know that all rendering strategies are first class citizens in React Router. +Whether you set `ssr` to `true` or `false` depends on you and your users' needs. Both strategies are perfectly valid. For the remainder of this tutorial we're going to use server-side rendering, but know that all rendering strategies are first class citizens in React Router. ## URL Params in Loaders @@ -896,6 +894,10 @@ export default function Contact({ loaderData, }: Route.ComponentProps) { const { contact } = loaderData; + /* const contact = { + ... + }; + */ // existing code } @@ -931,7 +933,7 @@ Now, if the user isn't found, code execution down this path stops and React Rout We'll create our first contact in a second, but first let's talk about HTML. -React Router emulates HTML Form navigation as the data mutation primitive, which used to be the only way prior to the JavaScript cambrian explosion. Don't be fooled by the simplicity! Forms in React Router give you the UX capabilities of client rendered apps with the simplicity of the "old school" web model. +React Router emulates HTML Form navigation as the data mutation primitive, which used to be the only way prior to the JavaScript cambrian explosion. Don't be fooled by the simplicity! Forms in React Router give you the UX capabilities of client-rendered apps with the simplicity of the "old school" web model. While unfamiliar to some web developers, HTML `form`s actually cause a navigation in the browser, just like clicking a link. The only difference is in the request: links can only change the URL while `form`s can also change the request method (`GET` vs. `POST`) and the request body (`POST` form data). @@ -1094,10 +1096,11 @@ The edit route we just created already renders a `form`. All we need to do is ad 👉 **Add an `action` function to the edit route** -```tsx filename=app/routes/edit-contact.tsx lines=[1,4,8,6-15] +```tsx filename=app/routes/edit-contact.tsx lines=[1,5,7-15] import { Form, redirect } from "react-router"; +// import { Form } from "react-router"; // existing imports - +// import { getContact } from "../data"; import { getContact, updateContact } from "../data"; export async function action({ @@ -1168,7 +1171,7 @@ export async function action({ params, request, }: Route.ActionArgs) { - invariant(params.contactId, "Missing contactId param"); + //invariant(params.contactId, "Missing contactId param"); // What is this? const formData = await request.formData(); const updates = Object.fromEntries(formData); await updateContact(params.contactId, updates); @@ -1188,8 +1191,10 @@ Now that we know how to redirect, let's update the action that creates new conta 👉 **Redirect to the new record's edit page** -```tsx filename=app/root.tsx lines=[6,12] +```tsx filename=app/root.tsx lines=[8,14] import { + Form, // not really needed anymore + Link, // not really needed anymore Outlet, Scripts, ScrollRestoration, @@ -1218,7 +1223,7 @@ Now that we have a bunch of records, it's not clear which one we're looking at i ```tsx filename=app/layouts/sidebar.tsx lines=[1,17-26,28] import { Form, Link, NavLink, Outlet } from "react-router"; - +//import { Form, Link, Outlet } from "react-router"; // existing imports and exports export default function SidebarLayout({ @@ -1262,7 +1267,7 @@ Note that we are passing a function to `className`. When the user is at the URL ## Global Pending UI -As the user navigates the app, React Router will _leave the old page up_ as data is loading for the next page. You may have noticed the app feels a little unresponsive as you click between the list. Let's provide the user with some feedback so the app doesn't feel unresponsive. +As the user navigates the app, React Router will _leave the old page up_ as data is loading for the next page. You may have noticed the app seemed a little unresponsive as you clicked between the list. Let's provide the user with some feedback so the app doesn't feel unresponsive. React Router is managing all the state behind the scenes and reveals the pieces you need to build dynamic web apps. In this case, we'll use the [`useNavigation`][use-navigation] hook. @@ -1301,7 +1306,7 @@ export default function SidebarLayout({ [`useNavigation`][use-navigation] returns the current navigation state: it can be one of `"idle"`, `"loading"` or `"submitting"`. -In our case, we add a `"loading"` class to the main part of the app if we're not idle. The CSS then adds a nice fade after a short delay (to avoid flickering the UI for fast loads). You could do anything you want though, like show a spinner or loading bar across the top. +In our case, we add a `"loading"` class to the main part of the app if it's not idle. The CSS then adds a nice fade after a short delay (to avoid flickering the UI for fast loads). You could do anything you want though, like show a spinner or loading bar across the top. @@ -1348,7 +1353,6 @@ export default [ "contacts/:contactId/destroy", "routes/destroy-contact.tsx", ), - // existing routes ] satisfies RouteConfig; ``` @@ -1376,7 +1380,7 @@ When the user clicks the submit button: 2. The `
` matches the new route at `contacts/:contactId/destroy` and sends it the request 3. After the `action` redirects, React Router calls all the `loader`s for the data on the page to get the latest values (this is "revalidation"). `loaderData` in `routes/contact.tsx` now has new values and causes the components to update! -Add a `Form`, add an `action`, React Router does the rest. +Add a `Form`, add an `action`, and React Router does the rest. ## Cancel Button @@ -1386,9 +1390,10 @@ We'll need a click handler on the button as well as [`useNavigate`][use-navigate 👉 **Add the cancel button click handler with `useNavigate`** -```tsx filename=app/routes/edit-contact.tsx lines=[1,8,15] +```tsx filename=app/routes/edit-contact.tsx lines=[1,9,16] import { Form, redirect, useNavigate } from "react-router"; -// existing imports & exports +//import { Form, redirect } from "react-router"; +// existing imports, and existing exports except for `EditContact` export default function EditContact({ loaderData, @@ -1401,9 +1406,7 @@ export default function EditContact({ {/* existing elements */}

- +

); @@ -1439,7 +1442,7 @@ Since it's not `
`, React Router emulates the browser by seri 👉 **Filter the list if there are `URLSearchParams`** ```tsx filename=app/layouts/sidebar.tsx lines=[3-8] -// existing imports & exports +// existing imports, and the existing exports except for `loader` export async function loader({ request, @@ -1447,6 +1450,8 @@ export async function loader({ const url = new URL(request.url); const q = url.searchParams.get("q"); const contacts = await getContacts(q); +//export async function loader() { + //const contacts = await getContacts(); return { contacts }; } @@ -1455,7 +1460,7 @@ export async function loader({ -Because this is a `GET`, not a `POST`, React Router _does not_ call the `action` function. Submitting a `GET` `form` is the same as clicking a link: only the URL changes. +Because this is a `GET` request and not a `POST` request, React Router _does not_ call the `action` function. Submitting a `GET` `form` is the same as clicking a link: only the URL changes. This also means it's a normal page navigation. You can click the back button to get back to where you were. @@ -1463,14 +1468,14 @@ This also means it's a normal page navigation. You can click the back button to There are a couple of UX issues here that we can take care of quickly. -1. If you click back after a search, the form field still has the value you entered even though the list is no longer filtered. -2. If you refresh the page after searching, the form field no longer has the value in it, even though the list is filtered +1. If you click back after a search, the form field will still have the value you entered even though the list is no longer filtered. +2. If you refresh the page after searching, the form field will no longer have the value in it, even though the list is filtered. -In other words, the URL and our input's state are out of sync. +In other words, the URL's and our input's states are out of sync. -Let's solve (2) first and start the input with the value from the URL. +Let's solve (2) first and initialize the input with the value from the URL. -👉 **Return `q` from your `loader`, set it as the input's default value** +👉 **Return `q` from your `loader` and set it as the input's default value** ```tsx filename=app/layouts/sidebar.tsx lines=[9,15,26] // existing imports & exports @@ -1518,16 +1523,14 @@ export default function SidebarLayout({ The input field will show the query if you refresh the page after a search now. -Now for problem (1), clicking the back button and updating the input. We can bring in `useEffect` from React to manipulate the input's value in the DOM directly. +Now for problem (1): clicking the back button and updating the input. We can bring in `useEffect` from React to manipulate the input's value in the DOM directly. 👉 **Synchronize input value with the `URLSearchParams`** -```tsx filename=app/layouts/sidebar.tsx lines=[2,12-17] +```tsx filename=app/layouts/sidebar.tsx lines=[2,10-15] // existing imports import { useEffect } from "react"; -// existing imports & exports - export default function SidebarLayout({ loaderData, }: Route.ComponentProps) { @@ -1547,7 +1550,7 @@ export default function SidebarLayout({ > 🤔 Shouldn't you use a controlled component and React State for this? -You could certainly do this as a controlled component. You will have more synchronization points, but it's up to you. +You certainly could implement this using a controlled component. You will have more synchronization points, but it's up to you.
@@ -1614,7 +1617,7 @@ We've got a product decision to make here. Sometimes you want the user to submit We've seen `useNavigate` already, we'll use its cousin, [`useSubmit`][use-submit], for this. -```tsx filename=app/layouts/sidebar.tsx lines=[7,16,27-29] +```tsx filename=app/layouts/sidebar.tsx lines=[7,16,25-29] import { Form, Link, @@ -1623,7 +1626,7 @@ import { useNavigation, useSubmit, } from "react-router"; -// existing imports & exports +// existing imports, and existing exports except for `SidebarLayout` export default function SidebarLayout({ loaderData, @@ -1660,17 +1663,17 @@ export default function SidebarLayout({ As you type, the `form` is automatically submitted now! -Note the argument to [`submit`][use-submit]. The `submit` function will serialize and submit any form you pass to it. We're passing in `event.currentTarget`. The `currentTarget` is the DOM node the event is attached to (the `form`). +Note the argument to [`submit`][use-submit]. The `submit` function will serialize and submit any form you pass to it. We're passing in `event.currentTarget`. The `currentTarget` is the DOM node the event is attached to (i.e., the `form`). -## Adding Search Spinner +## Adding a Search Spinner -In a production app, it's likely this search will be looking for records in a database that is too large to send all at once and filter client side. That's why this demo has some faked network latency. +In a production app, a given search of a database will likely return a set of records that is too large to send all at once to a client and be filtered client-side. This is why this demo has some faked network latency. -Without any loading indicator, the search feels kinda sluggish. Even if we could make our database faster, we'll always have the user's network latency in the way and out of our control. +Without any loading indicator, the search will seem a bit sluggish. Even if we could make our database faster, the the user's network latency will always be in the way and out of our control. For a better user experience, let's add some immediate UI feedback for the search. We'll use [`useNavigation`][use-navigation] again. -👉 **Add a variable to know if we're searching** +👉 **Add a variable to track the status of a search operation** ```tsx filename=app/layouts/sidebar.tsx lines=[9-13] // existing imports & exports @@ -1818,7 +1821,7 @@ export default function SidebarLayout({ } ``` -After a quick check if this is the first search or not, we decide to replace. Now the first search will add a new entry, but every keystroke after that will replace the current entry. Instead of clicking back 7 times to remove the search, users only have to click back once. +After a quick check to see if this is the first search or not, we decide to replace. Now the first search will add a new entry, but every keystroke after that will replace the current entry. Instead of clicking back 7 times to remove the search, users only have to click back once. ## `Form`s Without Navigation @@ -1830,9 +1833,9 @@ The ★ button on the contact page makes sense for this. We aren't creating or d 👉 **Change the `` form to a fetcher form** -```tsx filename=app/routes/contact.tsx lines=[1,10,14,26] +```tsx filename=app/routes/contact.tsx lines=[1,10,15,27] import { Form, useFetcher } from "react-router"; - +// import { Form } from "react-router"; // existing imports & exports function Favorite({ @@ -1844,6 +1847,7 @@ function Favorite({ const favorite = contact.favorite; return ( + {/* */}