react router6.x官方DEMO

news2024/11/19 7:46:42

Tutorial v6.4.2 | React Router

初始化项目

import React from "react";
import ReactDOM from "react-dom/client";
import {
  createBrowserRouter,
  RouterProvider,
  Route,
} from "react-router-dom";
import "./index.css";


const router = createBrowserRouter([
  {
    path: "/",
    element: <div>Hello world!</div>,
  },
]);

ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
);

准备额外文件

src目录下需要创建和编辑index.csscontacts.js文件

css

js

创建DEMO主界面并修改路由

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
  },
]);

routes/root.tsx 定义Root组件

export default function Root() {
    return (
      <>
        <div id="sidebar">
          <h1>React Router Contacts</h1>
          <div>
            <form id="search-form" role="search">
              <input
                id="q"
                aria-label="Search contacts"
                placeholder="Search"
                type="search"
                name="q"
              />
              <div
                id="search-spinner"
                aria-hidden
                hidden={true}
              />
              <div
                className="sr-only"
                aria-live="polite"
              ></div>
            </form>
            <form method="post">
              <button type="submit">New</button>
            </form>
          </div>
          <nav>
            <ul>
              <li>
                <a href={`contacts/1`}>Your Name</a>
              </li>
              <li>
                <a href={`contacts/2`}>Your Friend</a>
              </li>
            </ul>
          </nav>
        </div>
        <div id="detail"></div>
      </>
    );
  }

现在看起来

在这里插入图片描述

添加error界面

函数时编程,useRouteError函数获取出错信息

import { useRouteError } from "react-router-dom";

export default function ErrorPage() {
  const error = useRouteError();
  console.error(error);

  return (
    <div id="error-page">
      <h1>Oops!</h1>
      <p>Sorry, an unexpected error has occurred.</p>
      <p>
        <i>{error.statusText || error.message}</i>
      </p>
    </div>
  );
}

注册error界面

main.tsx

import ErrorPage from "./error-page";


const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    errorElement: <ErrorPage />,
  },
]);

当路由出错时:

在这里插入图片描述

Note that useRouteError provides the error that was thrown. When the user navigates to routes that don’t exist you’ll get an error response with a “Not Found” statusText. We’ll see some other errors later in the tutorial and discuss them more.

For now, it’s enough to know that pretty much all of your errors will now be handled by this page instead of infinite spinners, unresponsive pages, or blank screens 🙌

联系人组件

声明组件routes/contact.tsx

import { Form } from "react-router-dom";

export default function Contact() {
  const contact = {
    first: "Your",
    last: "Name",
    avatar: "https://placekitten.com/g/200/200",
    twitter: "your_handle",
    notes: "Some notes",
    favorite: true,
  };

  return (
    <div id="contact">
      <div>
        <img
          key={contact.avatar}
          src={contact.avatar || null}
        />
      </div>

      <div>
        <h1>
          {contact.first || contact.last ? (
            <>
              {contact.first} {contact.last}
            </>
          ) : (
            <i>No Name</i>
          )}{" "}
          <Favorite contact={contact} />
        </h1>

        {contact.twitter && (
          <p>
            <a
              target="_blank"
              href={`https://twitter.com/${contact.twitter}`}
            >
              {contact.twitter}
            </a>
          </p>
        )}

        {contact.notes && <p>{contact.notes}</p>}

        <div>
          <Form action="edit">
            <button type="submit">Edit</button>
          </Form>
          <Form
            method="post"
            action="destroy"
            onSubmit={(event) => {
              if (
                !confirm(
                  "Please confirm you want to delete this record."
                )
              ) {
                event.preventDefault();
              }
            }}
          >
            <button type="submit">Delete</button>
          </Form>
        </div>
      </div>
    </div>
  );
}

function Favorite({ contact }) {
  // yes, this is a `let` for later
  let favorite = contact.favorite;
  return (
    <Form method="post">
      <button
        name="favorite"
        value={favorite ? "false" : "true"}
        aria-label={
          favorite
            ? "Remove from favorites"
            : "Add to favorites"
        }
      >
        {favorite ? "★" : "☆"}
      </button>
    </Form>
  );
}

注册路由组件

import Contact from "./routes/contact";

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    errorElement: <ErrorPage />,
  },
  {
    path: "contacts/:contactId",
    element: <Contact />,
  },
]);

现在contacts会单独在一个页面中显示,我们希望他在Root组件右侧显示,即搜索后,在右侧显示联系人信息。

嵌套路由Nested Routes

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    errorElement: <ErrorPage />,
    children: [
      {
        path: "contacts/:contactId",
        element: <Contact />,
      },
    ],
  },
]);

原本的Root组件结构

在这里插入图片描述

我们的目的是在Root组件里面显示下级路由组件Contacts的内容,官方是这样做的:

import { Outlet } from "react-router-dom";

<div id="detail">
	<Outlet />
</div>

在一个空div中,添加了Outlet组件,那么我们可以猜到,子路由的组件将渲染到该位置。

效果:

在这里插入图片描述

Outlet组件的官方说明

Outlet v6.4.2 | React Router

An <Outlet> should be used in parent route elements to render their child route elements. This allows nested UI to show up when child routes are rendered. If the parent route matched exactly, it will render a child index route or nothing if there is no index route.

在父路由元素中应该使用<Outlet>来呈现子路由元素。这允许在呈现子路由时显示嵌套UI。如果父路由完全匹配,它将呈现子索引路由,如果没有索引路由则不呈现子索引路由。

修改左侧跳转标签

将标签替换为组件

 <ul>
  <li>
  	<Link to={`contacts/1`}>Your Name</Link>
  </li>
  <li>
  	<Link to={`contacts/2`}>Your Friend</Link>
  </li>
</ul>

根据id加载数据

URL段、布局、数据经常耦合在一起,例如:

URL SegmentComponentData
/<Root>list of contacts
contacts/:id<Contact>individual contact

因此,React Router定义了一些约定(data conventions) 帮助将数据传给路由组件,包括loaderuseLoaderData.

root.tsx中导出一个loader函数:

import { getContacts } from "../contacts";

export async function loader() {
  const contacts = await getContacts();
  return { contacts };
}

loader

loader v6.4.2 | React Router

Each route can define a “loader” function to provide data to the route element before it renders.

在路由组件渲染前(挂载)给组件传递数据。

路由组件获取数据并显示

root.tsx

import {
  useLoaderData,
} from "react-router-dom";

/* other code */

export default function Root() {
  const { contacts } = useLoaderData();
  return (
    <>
      <div id="sidebar">
        <h1>React Router Contacts</h1>
        {/* other code */}

        <nav>
          {contacts.length ? (
            <ul>
              {contacts.map((contact) => (
                <li key={contact.id}>
                  <Link to={`contacts/${contact.id}`}>
                    {contact.first || contact.last ? (
                      <>
                        {contact.first} {contact.last}
                      </>
                    ) : (
                      <i>No Name</i>
                    )}{" "}
                    {contact.favorite && <span>★</span>}
                  </Link>
                </li>
              ))}
            </ul>
          ) : (
            <p>
              <i>No contacts</i>
            </p>
          )}
        </nav>

        {/* other code */}
      </div>
    </>
  );
}

效果

在这里插入图片描述

由于getContacts没有接收到参数,所以返回的是空对象。显示无联系人

Data Writes + HTML Forms

Root组件中的New按钮触发了表单提交事件

								<div>
                    <form id="search-form" role="search">
                        <input
                            id="q"
                            aria-label="Search contacts"
                            placeholder="Search"
                            type="search"
                            name="q"
                        />
                        <div
                            id="search-spinner"
                            aria-hidden
                            hidden={true}
                        />
                        <div
                            className="sr-only"
                            aria-live="polite"
                        ></div>
                    </form>
                    <form method="post">
                        <button type="submit">New</button>
                    </form>
                </div>

Tutorial v6.4.2 | React Router

While unfamiliar to some web developers, HTML forms 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 forms can also change the request method (GET vs POST) and the request body (POST form data).

虽然对一些web开发人员来说并不熟悉,但HTML表单实际上会在浏览器中产生导航,就像点击链接一样。唯一的区别在于请求:链接只能更改URL,而表单还可以更改请求方法(GET vs POST)和请求体(POST表单数据)。

Instead of sending that POST to the Vite server to create a new contact, let’s use client side routing instead.

取消触发传统表达事件处理,把这个url链接交给路由处理。

创建Contacts

Tutorial v6.4.2 | React Router

root.tsx

import {
  Outlet,
  Link,
  useLoaderData,
  Form,
} from "react-router-dom";
import { getContacts, createContact } from "../contacts";

export async function action() {
  await createContact();
}

/* other code */

export default function Root() {
  const { contacts } = useLoaderData();
  return (
    <>
      <div id="sidebar">
        <h1>React Router Contacts</h1>
        <div>
          {/* other code */}
          <Form method="post">
            <button type="submit">New</button>
          </Form>
        </div>

        {/* other code */}
      </div>
    </>
  );
}

导出了一个事件处理函数action

main.ts

定义路由时,添加action属性

import Root, {
  loader as rootLoader,
  action as rootAction,
} from "./routes/root";

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    errorElement: <ErrorPage />,
    loader: rootLoader,
    action: rootAction,
    children: [
      {
        path: "contacts/:contactId",
        element: <Contact />,
      },
    ],
  },
]);

效果

在这里插入图片描述

能够添加联系人了,但是联系人属性没有保存。

The createContact method just creates an empty contact with no name or data or anything. But it does still create a record, promise!

🧐 Wait a sec … How did the sidebar update? Where did we call the action? Where’s the code to refetch the data? Where are useState, onSubmit and useEffect?!

这就是“old school web”编程模式出现的地方。正如我们前面所讨论的,阻止浏览器将请求发送到服务器,而是将其发送到您的路由操作。在web语义中,POST通常意味着某些数据正在发生变化。按照约定,React Router将此作为提示,在操作完成后自动重新验证页面上的数据。这意味着所有的useLoaderData钩子都会更新,UI会自动与您的数据保持同步!很酷。

点击record显示对应信息

点击联系人record后,链接变成contacts/xxxx

看一下路由声明:

[
  {
    path: "contacts/:contactId",
    element: <Contact />,
  },
];

These params are passed to the loader with keys that match the dynamic segment. For example, our segment is named :contactId so the value will be passed as params.contactId.

These params are most often used to find a record by ID. Let’s try it out.

contact.tsx

使用params

import { Form, useLoaderData } from "react-router-dom";
import { getContact } from "../contacts";

export async function loader({ params }) {
  return getContact(params.contactId);
}

export default function Contact() {
  // const contact = {
  //   first: "Your",
  //   last: "Name",
  //   avatar: "https://placekitten.com/g/200/200",
  //   twitter: "your_handle",
  //   notes: "Some notes",
  //   favorite: true,
  // };
  const contact = useLoaderData();
  // existing code
}

效果

在这里插入图片描述

数据存储

在这里插入图片描述

localforage

编辑数据

Just like creating data, you update data withForm. Let’s make a new route at contacts/:contactId/edit. Again, we’ll start with the component and then wire it up to the route config.

新建组件edit.tsx

import { Form, useLoaderData } from "react-router-dom";

export default function EditContact() {
  const contact = useLoaderData();

  return (
    <Form method="post" id="contact-form">
      <p>
        <span>Name</span>
        <input
          placeholder="First"
          aria-label="First name"
          type="text"
          name="first"
          defaultValue={contact.first}
        />
        <input
          placeholder="Last"
          aria-label="Last name"
          type="text"
          name="last"
          defaultValue={contact.last}
        />
      </p>
      <label>
        <span>Twitter</span>
        <input
          type="text"
          name="twitter"
          placeholder="@jack"
          defaultValue={contact.twitter}
        />
      </label>
      <label>
        <span>Avatar URL</span>
        <input
          placeholder="https://example.com/avatar.jpg"
          aria-label="Avatar URL"
          type="text"
          name="avatar"
          defaultValue={contact.avatar}
        />
      </label>
      <label>
        <span>Notes</span>
        <textarea
          name="notes"
          defaultValue={contact.notes}
          rows={6}
        />
      </label>
      <p>
        <button type="submit">Save</button>
        <button type="button">Cancel</button>
      </p>
    </Form>
  );
}

添加路由

main.tsx

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    errorElement: <ErrorPage />,
    loader: rootLoader,
    action: rootAction,
    children: [
      {
        path: "contacts/:contactId",
        element: <Contact />,
        loader: contactLoader,
      },
      {
        path: "contacts/:contactId/edit",
        element: <EditContact />,
        loader: contactLoader,
      },
    ],
  },
]);

效果

在这里插入图片描述

注意

(You might note we reused the contactLoader for this route. This is only because we’re being lazy in the tutorial. There is no reason to attempt to share loaders among routes, they usually have their own.)

保存edit数据

与之前相同,自定义表单Action事件,避免将请求发送给服务器

edit.tsx

import {
  Form,
  useLoaderData,
  redirect,
} from "react-router-dom";
import { updateContact } from "../contacts";

export async function action({ request, params }) {
  const formData = await request.formData();
  const updates = Object.fromEntries(formData);
  await updateContact(params.contactId, updates);
  return redirect(`/contacts/${params.contactId}`);
}

/* existing code */

之后与之前一样在main.tsx中声明action属性。

深入讨论

修改是如何生效的,以及页面如何触发重新渲染?

如果没有添加额外js代码。当表单提交时,浏览器将会创建FormData,并将其作为request的body发送给服务器。

React Router将发送给服务器的request转交给action,并阻止其发送给服务器。

export async function action({ request, params }) {
  const formData = await request.formData();
  const firstName = formData.get("first");
  const lastName = formData.get("last");
  // ...
}

除了fromaction(react router提供),所有api如:request, request.formData都是web平台提供的。

Loaders and actions can both return a Response (makes sense, since they received a Request!). The redirect helper just makes it easier to return a response that tells the app to change locations.

Without client side routing, if a server redirected after a POST request, the new page would fetch the latest data and render. As we learned before, React Router emulates this model and automatically revalidates the data on the page after the action. That’s why the sidebar automatically updates when we save the form. The extra revalidation code doesn’t exist without client side routing, so it doesn’t need to exist with client side routing either!

如果没有客户端路由,如果服务器在POST请求后重定向,新页面将获取最新数据并呈现。正如我们以前学到的,React Router模拟这个模型,并在操作之后自动重新验证页面上的数据。这就是为什么当我们保存表单时,侧栏会自动更新。在没有客户端路由的情况下,额外的重新验证代码是不存在的,所以在没有客户端路由的情况下,它也不需要存在!

添加record后重定向到编辑页面

root.tsx中的action返回重定向

import {
  Outlet,
  Link,
  useLoaderData,
  Form,
  redirect,
} from "react-router-dom";
import { getContacts, createContact } from "../contacts";

export async function action() {
  const contact = await createContact();
  return redirect(`/contacts/${contact.id}/edit`);
}

使用NavLink高亮所选联系人标签

<NavLink
  to={`contacts/${contact.id}`}
  className={({ isActive, isPending }) =>
    isActive
      ? "active"
      : isPending
      	? "pending"
        : ""
  }
>
{/* other code */}
</NavLink>

当我们在<NavLink>所指定的路由时,isActive会被设置为true,When it’s about to be active (the data is still loading) then isPending will be true. This allows us to easily indicate where the user is, as well as provide immediate feedback on links that have been clicked but we’re still waiting for data to load.

效果

在这里插入图片描述

全局等待UI

当用户导航应用程序时,React Router将离开旧的页面,因为数据正在为下一页加载。你可能已经注意到,当你在列表之间点击时,应用程序感觉有点没有反应。让我们为用户提供一些反馈,这样应用程序就不会感到没有响应。

React Router在幕后管理所有的状态,并揭示你构建动态web应用所需要的部分。在本例中,我们将使用usenavnavigation钩子。

root.tsx

import {
  // existing code
  useNavigation,
} from "react-router-dom";

// existing code

export default function Root() {
  const { contacts } = useLoaderData();
  const navigation = useNavigation();

  return (
    <>
      <div id="sidebar">{/* existing code */}</div>
      <div
        id="detail"
        className={
          navigation.state === "loading" ? "loading" : ""
        }
      >
        <Outlet />
      </div>
    </>
  );
}

useNavigation returns the current navigation state: it can be one of "idle" | "submitting" | "loading".

表示当前路由状态

在这里插入图片描述

#detail.loading {
  opacity: 0.25;
  transition: opacity 200ms;
  transition-delay: 200ms;
}

删除记录

在这里插入图片描述

edit按钮事件以及处理好,接下来处理删除事件,同样是发送post请求。

contact.tsx

<Form
  method="post"
  action="destroy"
  onSubmit={(event) => {
    if (
      !confirm(
        "Please confirm you want to delete this record."
      )
    ) {
      event.preventDefault();
    }
  }}
>
  <button type="submit">Delete</button>
</Form>

这里直接在Form组件中写了action属性,因此发出的请求url是contact/:contactId/destroy

所有我们需要注册这个路由,并为这个路由添加action属性。

destroy.tsx

import { redirect } from "react-router-dom";
import { deleteContact } from "../contacts";

export async function action({ params }) {
  await deleteContact(params.contactId);
  return redirect("/");
}

main.tsx

      {
        path: "contacts/:contactId/destroy",
        action: deleteAction,
      },

尝试删除出错场景

手动抛出错误

destroy.tsx

export async function action({ params }) {
  throw new Error("oh dang!");
  await deleteContact(params.contactId);
  return redirect("/");
}

配置路由组件的错误处理组件

[
  /* other routes */
  {
    path: "contacts/:contactId/destroy",
    action: destroyAction,
    errorElement: <div>Oops! There was an error.</div>,
  },
];

效果

在这里插入图片描述

错误冒泡

当不为contacts/:contactId/destroy路由对应的errorElement时,error会像上级组件传递,即被在这里插入图片描述
处理。

效果为:

在这里插入图片描述

Index组件

在这里插入图片描述

当子路由没有任何匹配时,右侧是空白的,我们希望其显示一些默认内容,例如数据统计等。

像传统web一样,我们会显示目录下的index.html文件,在react路由中,我们可以声明index组件,当路由与当前路由完全匹配时(没有子路由匹配),显示index组件。

创建index组件

index.tsx

export default function Index() {
  return (
    <p id="zero-state">
      This is a demo for React Router.
      <br />
      Check out{" "}
      <a href="https://reactrouter.com/">
        the docs at reactrouter.com
      </a>
      .
    </p>
  );
}

声明index路由

 children: [
      {
        index: true,
        element: <Index></Index>
      },

效果

在这里插入图片描述

Note the { index:true } instead of { path: "" }. That tells the router to match and render this route when the user is at the parent route’s exact path, so there are no other child routes to render in the <Outlet>.

放弃编辑并返回

在这里插入图片描述

edit.tsx

import {
  Form,
  useLoaderData,
  redirect,
  useNavigate,
} from "react-router-dom";

export default function Edit() {
  const contact = useLoaderData();
  const navigate = useNavigate();

  return (
    <Form method="post" id="contact-form">
      {/* existing code */}

      <p>
        <button type="submit">Save</button>
        <button
          type="button"
          onClick={() => {
            navigate(-1);
          }}
        >
          Cancel
        </button>
      </p>
    </Form>
  );
}

Now when the user clicks “Cancel”, they’ll be sent back one entry in the browser’s history.

🧐 Why is there no event.preventDefault on the button?

A <button type="button">, while seemingly redundant, is the HTML way of preventing a button from submitting its form.

Two more features to go. We’re on the home stretch!

URL Search参数和Get请求

All of our interactive UI so far have been either links that change the URL or forms that post data to actions. The search field is interesting because it’s a mix of both: it’s a form but it only changes the URL, it doesn’t change data.

到目前为止,我们所有的交互UI都是更改URL的链接或将数据发布到操作的表单。搜索字段很有趣,因为它是两者的混合:它是一个表单,但它只改变URL,不改变数据。

look at传统form

Note the browser’s URL now contains your query in the URL as URLSearchParams:

http://127.0.0.1:5173/?q=ryan

root.tsx

<form id="search-form" role="search">
  <input
    id="q"
    aria-label="Search contacts"
    placeholder="Search"
    type="search"
    name="q"
  />
  <div id="search-spinner" aria-hidden hidden={true} />
  <div className="sr-only" aria-live="polite"></div>
</form>

As we’ve seen before, browsers can serialize forms by the name attribute of it’s input elements. The name of this input is q, that’s why the URL has ?q=. If we named it search the URL would be ?search=.

Note that this form is different from the others we’ve used, it does not have <form method="post">. The default method is "get". That means when the browser creates the request for the next document, it doesn’t put the form data into the request POST body, but into the URLSearchParams of a GET request.

使用GET请求方式的化,参数会加在路径中,而不是加在请求体中。

客户端路由处理Get请求

root.tsx

将form改为Form组件

<Form id="search-form" role="search">
  <input
    id="q"
    aria-label="Search contacts"
    placeholder="Search"
    type="search"
    name="q"
  />
  <div id="search-spinner" aria-hidden hidden={true} />
  <div className="sr-only" aria-live="polite"></div>
</Form>

获取参数

export async function loader({ request }) {
  const url = new URL(request.url);
  const q = url.searchParams.get("q");
  const contacts = await getContacts(q);
  return { contacts };
}

在这里插入图片描述

Because this is a GET, not a POST, React Router does not call the action. Submitting a GET form is the same as clicking a link: only the URL changes. That’s why the code we added for filtering is in the loader, not the action of this route.

This also means it’s a normal page navigation. You can click the back button to get back to where you were.

由于这是GET请求,不是POST请求,不会触发路由的action,提交GET表单和点击一个链接效果是相同的,只有url发送改变。这也是为什么我们只在loader中添加了代码,而不是action。

同步url参数和表单状态

目前存在的问题:

  1. 如果在搜索后单击返回,即使不再过滤列表,表单字段仍然具有您输入的值。
  2. 如果在搜索后刷新页面,表单字段中不再有该值,即使列表已被过滤。

即表单状态与url不同步。

解决方案

获取url中的参数,并将其填充到表单中

  • 在loader中返回url参数
  • 定义函数组件时,将loader返回的参数填充到表单中
export async function loader({ request }) {
  const url = new URL(request.url);
  const q = url.searchParams.get("q");
  const contacts = await getContacts(q);
  return { contacts, q };
}
const { contacts, q } = useLoaderData();

defaultValue={q}

根据测试目前是没有问题的,但是官方提到这只解决了第二个问题,第一个问题没有解决。

但是假设当前url是:http://localhost:5173/,设搜索后的url是http://localhost:5173/?q=12,

返回后是没有问题的,因为获取到的q是空,填充到表单也是空。

这是因为没有将form改为Form,点击回车后触发了form表单的事件,触发页面更新,而使用Form组件后,页面不会更新,不会重新触发loader,因此页面不会更新。(会重新进行渲染,但根据react的Diff算法,不会刷新DOM

在这里插入图片描述

当我们刷新后,表单输入才会变成空。

useEffect

Hook API 索引 – React (reactjs.org)

该 Hook 接收一个包含命令式、且可能有副作用代码的函数。

在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。

使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。你可以把 effect 看作从 React 的纯函数式世界通往命令式世界的逃生通道。

默认情况下,effect 将在每轮渲染结束后执行,但你可以选择让它 在只有某些值改变的时候 才执行。

清除 effect

通常,组件卸载时需要清除 effect 创建的诸如订阅或计时器 ID 等资源。要实现这一点,useEffect 函数需返回一个清除函数。以下就是一个创建订阅的例子:

执行时机

componentDidMountcomponentDidUpdate 不同的是,传给 useEffect 的函数会在浏览器完成布局与绘制之后,在一个延迟事件中被调用。这使得它适用于许多常见的副作用场景,比如设置订阅和事件处理等情况,因为绝大多数操作不应阻塞浏览器对屏幕的更新。

effect 的条件执行

默认情况下,effect 会在每轮组件渲染完成后执行。这样的话,一旦 effect 的依赖发生变化,它就会被重新创建。

然而,在某些场景下这么做可能会矫枉过正。比如,在上一章节的订阅示例中,我们不需要在每次组件更新时都创建新的订阅,而是仅需要在 source prop 改变时重新创建。

要实现这一点,可以给 useEffect 传递第二个参数,它是 effect 所依赖的值数组。更新后的示例如下:

解决问题1

使用useEffect的条件执行

  useEffect(() => {
    document.getElementById("q").value = q;
  }, [q]);

完全受控组件模式

const [query, setQuery] = useState(q);

            <input
              id="q"
              aria-label="Search contacts"
              placeholder="Search"
              type="search"
              name="q"
              value={query}
              onChange={(e) => {
                setQuery(e.target.value);
              }}
            />

每当表单状态改变时,触发搜索

onChange={(event) => {
	submit(event.currentTarget.form);
}}

Now as you type, the form is submitted automatically!

Note the argument to submit. We’re passing in event.currentTarget.form. The currentTarget is the DOM node the event is attached to, and the currentTarget.form is the input’s parent form node. The submit function will serialize and submit any form you pass to it.

增加搜索提示

在生产项目中,搜索是需要花一定时间的,为了获得更好的用户体验,让我们为搜索添加一些即时UI反馈。为此,我们将再次使用useNavigation。

const searching =
    navigation.location &&
    new URLSearchParams(navigation.location.search).has(
      "q"
    );

The navigation.location will show up when the app is navigating to a new URL and loading the data for it. It then goes away when there is no pending navigation anymore.

navigation.location 会在正在当前应用正在向另一个链接导航,并且loader正在执行时为真。

组件渲染的执行次数

root.tsx

export default function Root() {
    console.log('@@@')

当我们在搜索栏键入1后:

在这里插入图片描述

我们使用了useNavigation,他返回了一个state,当它改变后,触发了页面更新。

避免产生太多路由记录

const isFirstSearch = q == null;
 submit(event.currentTarget.form, {
  replace: !isFirstSearch,
});

不使用导航触发更新

到目前为止我们都是通过表单更高url获取POST请求,在历史堆栈中添加记录,实现mutations(the times we change data)。

那么我们如何不借助导航让数据发送改变呢。

useFetcher hook函数让我们能够直接与loader函数通信。

在这里插入图片描述

喜欢按钮符合我们这样做的原则:我们并不是在创建或删除新记录,也不想更改页面,我们只是想更改正在查看的页面上的数据。

实施

编辑contact.tsx

import {
  useLoaderData,
  Form,
  useFetcher,
} from "react-router-dom";

function Favorite({ contact }) {
  const fetcher = useFetcher();
  let favorite = contact.favorite;

  return (
    <fetcher.Form method="post">
      <button
        name="favorite"
        value={favorite ? "false" : "true"}
        aria-label={
          favorite
            ? "Remove from favorites"
            : "Add to favorites"
        }
      >
        {favorite ? "★" : "☆"}
      </button>
    </fetcher.Form>
  );
}

和往常一样,我们的表单有带有名称prop的字段。这个表单将发送带有favorite key的formData,它要么是"true",要么是" false"。因为它有method=“post”,它会调用action。因为没有 <fetcher.Form action="..."> prop,它将发送到呈现表单的路由。

定义action

export async function action({ request, params }) {
  let formData = await request.formData();
  return updateContact(params.contactId, {
    favorite: formData.get("favorite") === "true",
  });
}

声明路由action

import Contact, {
  loader as contactLoader,
  action as contactAction,
} from "./routes/contact";


与Form组件唯一不同的是:它不是一个导航——URL不会改变,历史堆栈不受影响。

点击喜欢后的界面优化(乐观UI策略,optimistic UI)

点击喜欢后,需要时间处理这个请求,这段时间界面没有任何反馈,一段时间后才显示喜欢图标。

为了增加反馈,我们需要使用fetcher的状态,就行我们之前使用navigation的状态一样。

fetcher知道提交给action的表单数据,所以可以在fetcher. formdata上获得。我们将使用它立即更新star的状态,即使请求还没有完成。如果更新最终失败,UI将恢复到真实的数据

// existing code

function Favorite({ contact }) {
  const fetcher = useFetcher();

  let favorite = contact.favorite;
  if (fetcher.formData) {
    favorite = fetcher.formData.get("favorite") === "true";
  }

  return (
    <fetcher.Form method="post">
      <button
        name="favorite"
        value={favorite ? "false" : "true"}
        aria-label={
          favorite
            ? "Remove from favorites"
            : "Add to favorites"
        }
      >
        {favorite ? "★" : "☆"}
      </button>
    </fetcher.Form>
  );
}

如果你现在点击按钮,你应该会看到星星立即变为新的状态。我们并不总是呈现实际的数据,而是检查fetcher是否有任何formData被提交,如果有,我们将使用它。当动作完成时,fetcher.formData将不再存在,我们将回到使用实际数据。因此,即使您在乐观的UI代码中编写了错误,它最终也会回到正确的状态。

用户不存在Error

当我们试图指定url查看一个不存在的用户,会引发error,并跳转到error界面。

在这里插入图片描述

这是因为我们loader获取到的contact对象是null。

对于这种已知的错误,我们希望应用进行明确的显示,并主动抛出错误。:

export async function loader({ params }) {
  const contact = await getContact(params.contactId);
  if (!contact) {
    throw new Response("", {
      status: 404,
      statusText: "Not Found",
    });
  }
  return contact;
}

在这里插入图片描述

但是,这个错误是顶级路由处理的,我们的界面无法显示其他任何内容,当我们指定这是contact组件的错误,我们只希望在contact组件位置显示该错误界面。

Pathless Routes

One last thing. The last error page we saw would be better if it rendered inside the root outlet, instead of the whole page. In fact, every error in all of our child routes would be better in the outlet, then the user has more options than hitting refresh.

We’d like it to look like this:

在这里插入图片描述

我们可以向每个组件都添加errorElement: <ErrorPage />,属性,但这显然是重复、代码冗余的。

有一个更清洁的方法。可以在没有路径的情况下使用路由,这使得它们可以参与UI布局,而不需要在URL中添加新的路径段:

createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    loader: rootLoader,
    action: rootAction,
    errorElement: <ErrorPage />,
    children: [
      {
        errorElement: <ErrorPage />,
        children: [
          { index: true, element: <Index /> },
          {
            path: "contacts/:contactId",
            element: <Contact />,
            loader: contactLoader,
            action: contactAction,
          },
          /* the rest of the routes */
        ],
      },
    ],
  },
]);

这样的话,error在冒泡时,就被无路径路由捕获了。

在这里插入图片描述

JSX Routes

在之前,我们的路由都是在最开始就完成了全部定义。

对于我们的最后一个技巧,许多人更喜欢用JSX配置他们的路由。你可以用createRoutesFromElements做到这一点。在配置路由时,JSX和对象之间没有功能上的区别,这只是一种风格偏好。

import {
  createRoutesFromElements,
  createBrowserRouter,
} from "react-router-dom";

const router = createBrowserRouter(
  createRoutesFromElements(
    <Route
      path="/"
      element={<Root />}
      loader={rootLoader}
      action={rootAction}
      errorElement={<ErrorPage />}
    >
      <Route errorElement={<ErrorPage />}>
        <Route index element={<Index />} />
        <Route
          path="contacts/:contactId"
          element={<Contact />}
          loader={contactLoader}
          action={contactAction}
        />
        <Route
          path="contacts/:contactId/edit"
          element={<EditContact />}
          loader={contactLoader}
          action={editAction}
        />
        <Route
          path="contacts/:contactId/destroy"
          action={destroyAction}
        />
      </Route>
    </Route>
  )
);

总结

  • loader给路由组件传递数据
  • error page
  • Form
  • fetcher.Form
  • NavLink
  • Optimistic UI
  • redirect
  • useEffect
  • 路由params获取
  • Form表单状态与URL同步
  • 获取导航状态

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/409273.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

用CSS设置颜色、背景和图像效果

&#x1f4dc;个人简介 ⭐️个人主页&#xff1a;微风洋洋&#x1f64b;‍♂️ &#x1f351;博客领域&#xff1a;编程基础&#x1f4a1;,后端&#x1f4a1;,大数据,信息安全 &#x1f345;写作风格&#xff1a;干货,干货,还是tmd的干货 &#x1f338;精选专栏&#xff1a;【J…

js数组常用方法(19种)|你会的到底有多少呢?

一、改变原数组的方法 1.push&#xff08;&#xff09; 末尾添加数据 语法: 数组名.push(数据) 作用: 就是往数组末尾添加数据 返回值: 就是这个数组的长度 //push var arr [10, 20, 30, 40] res arr.push(20) console.log(arr);//[10,20,30,40,20] console.log(res);//52. …

【Cesium】使用TLE轨道两行数计算轨道信息,并生成CZML格式文件

TLE为轨道两行数&#xff0c;简单的说是用两行数字表示轨道的相关信息&#xff0c;本文即用轨道两行数来计算任一时刻卫星的位置信息和速度信息&#xff0c;并生成CZML文件能够读取的格式 1、satellite.js库简介 简而言之&#xff0c;satellite.js库可以根据TLE轨道两行数&…

vue 项目适配笔记本1920*1080 125%缩放

前言 在台式机上开发pc端项目时&#xff0c;由于是1920*1080的分辨路和100缩放&#xff0c;看起来是没有问题。在笔记本上有问题 因为现在很多14寸的笔记本&#xff0c;出厂默认就是125%或150%的显示。导致很多时候我们的项目&#xff0c;自己开发的时候都是按照100%比例来开发…

Vue使用Serial连接串口

本来只是随手记录一下&#xff0c;发现看的人多了&#xff0c;想着还是修复一下bug吧&#xff0c;供各位看官指正 2022-10-24本次更新: 1、修复在不支持Serial的情况下&#xff0c;控制台报错 2022-09-19本次更新: 1、修复了传输数据接收分隔的情况(增加数据缓存) 2、修复串口连…

【中兴】web训练营~一文带你走进前端 | 百图制作

&#x1f4e2;作者简介&#xff1a;物联网领域创作者&#xff0c;&#x1f3c5;阿里云专家博主&#x1f3c5; &#x1f3c5;华为云享专家&#x1f3c5; ✒️个人主页&#xff1a;Choice~ &#x1f310;格言&#xff1a;可正因为难&#xff0c;才有价值&#xff01;&#x1f536…

Linux 使用Nginx部署web(vue、react)项目

前言 本文基于&#xff1a;操作系统 CentOS 7.6 使用的工具&#xff1a;Xshell7、Xftp7 1.安装所需依赖 安装gcc yum -y install gcc安装pcre、pcre-devel yum -y install pcre pcre-devel安装zlib、zlib-devel yum install -y zlib zlib-devel安装openssl、openssl-dev…

【uni-app】点击左上角返回按钮,弹出弹窗或者是携带参数返回上一页

目录 1、弹出弹窗 2、把这一页的数据带回到上一页&#xff08;获取下一页的数据 &#xff09; 3、跳转页面并携带参数&#xff0c;接受页获取参数 1、弹出弹窗 当我返回上一页的时候需要做一个判断是否需要保存 onBackPress 只支持APP和H5 但不支持小程序 &#xff0c;可以…

Java web—访问http://localhost:8080/xx/xx.jsp报404错误问题

由于我们在eclipse ee中把项目部署在web端经常会出现报404错误。 原因为&#xff1a; 404状态码是一种http状态码&#xff0c;其意思是&#xff1a; 所请求的页面不存在或已被删除。通俗的讲就是当用户输入了错误的链接时&#xff0c;返回的页面。 以下描述几种情况&#xff1a…

IDEA从零到精通(24)之lombok插件的安装与使用

文章目录作者简介引言导航概述安装插件使用小结导航热门专栏推荐作者简介 作者名&#xff1a;编程界明世隐 简介&#xff1a;CSDN博客专家&#xff0c;从事软件开发多年&#xff0c;精通Java、JavaScript&#xff0c;博主也是从零开始一步步把学习成长、深知学习和积累的重要性…

【Vue】 组件封装

目录1 组件封装1.1 全局注册1.2 局部注册1.2.1 命名1.2.2 引用组件1.2.2.1 传统写法1.2.2.2 setup1.2.2.3 easycom1.3 父子组件间的数据传递1.3.1 子组件 data() 中设置数据1.3.2 父组件通过 prop 将数据传递给子组件1.3.3 子组件不能直接修改 prop 中的值1.3.4 子组件通过 emi…

Pinia中action使用详解

actions的使用 动作相当于组件中的方法。它们可以使用actionsin 属性进行定义。 并且在pinia中的action既可以有同步函数也可以有异步函数。 在actions中可以通过this访问该仓库所有实例 export const useUsers defineStore(users,{state:()>{userData:null},actions:{a…

【SpringMVC】集成Web、MVC执行流程、数据响应、数据交互

文章目录前言一.Spring集成Web二.对于SpringMVC的理解三.MVC执行流程&#xff08;&#x1f3f3;️‍&#x1f308;&#xff09;1.组件解析2.RequestMapping四.SpringMVC数据响应页面跳转回写数据五.SpringMVC获得请求数据前言 SpringMVC确实很麻烦&#xff0c;零碎的点太多 一…

小程序自定义tabbar导航栏、动态控制tabbar功能实现(uniapp)

uniapp开发小程序&#xff0c;不同角色/已登录未登录&#xff0c;都有不一样的底部导航栏&#xff0c;这些情况下就需要自行定义tabbar&#xff0c;从而实现动态tabbar的实现。 1.首先我们需要在pages.json配置tabbar 我这里并没有开启custom(自定义)&#xff0c;不开启的话&a…

vue实现思维导图

介绍 前景&#xff1a; 仿幕布实现思维导图效果 技术实现&#xff1a;jsmind 完整代码&#xff1a;vue-jsmind 参考文章&#xff1a; 在vue中使用jsmind组织架构或思维导图 实现效果&#xff1a; 功能描述&#xff1a; 编辑、删除、插入、拖拽、展开/收起节点分布结构切换…

数字IC前端面试问题总结

本篇主要参考了 1、新芯设计(3条消息) 新芯设计的博客_CSDN博客-如何成为一名高级数字 IC 设计工程师,数字 IC 技能拓展,基于 SoC 的卷积神经网络车牌识别系统设计领域博主 2、小汪的IC自习室 (3条消息) 小汪的IC自习室的博客_CSDN博客-数字IC设计,SystemVerilog & I…

前端使用xlsx插件读取excel文件数据(保姆级教程)

本人属于一个实习菜鸟&#xff0c;大神请谨慎阅读............ 在开发过程中&#xff0c;难免会碰到用前端来处理excel文件的需求&#xff0c;我们需要解析出excel文件的内容然后在以对象的形式展示或者与后端对接 功能的实现思路&#xff1a; 文件选择 > FileReader对象…

微信小程序中使用vant框架,方法步骤清晰,简单适用

1.说到vant框架相信大家应该并不陌生了吧&#xff0c;做过移动端开发的小伙伴们应该都知道它吧。 2.Vant 是有赞前端团队开源的移动端组件库&#xff0c;于 2017 年开源&#xff0c;已持续维护 4 年时间。Vant 对内承载了有赞所有核心业务&#xff0c;对外服务十多万开发者&am…

Vue 权限菜单(动态路由)详解

今天记录一下Vue的权限菜单&#xff08;动态路由&#xff09;&#xff0c;在我们写后台的时候用的比较多&#xff0c;Vue的权限菜单分两种&#xff0c;一种是通过本地进行&#xff0c;根据账号的权限进行筛选出可用的权限&#xff0c;组合菜单并在页面上渲染显示&#xff0c;另…

Vue3 从入门到放弃 (第一篇.环境准备)

什么是 Vue&#xff1f;# Vue (发音为 /vjuː/&#xff0c;类似 view) 是一款用于构建用户界面的 JavaScript 框架。它基于标准 HTML、CSS 和 JavaScript 构建&#xff0c;并提供了一套声明式的、组件化的编程模型&#xff0c;帮助你高效地开发用户界面。无论是简单还是复杂的…