React Router 完美教程(下)

我们书接上回,继续我们的React Router 路由之路:

我们到目前为止都没有用到 state、useEffect、redux等状态管理器。但也达到了我们的设计目的。

注意,action 返回的结果 可以在组件中使用 useActionData() 来获取。就像 useLoaderData() 的使用一样。

loader 中的 URL参数

接着上回的示例文件, 新建几个无名联系人。虽然联系人信息没有变化,但我们仔细查看地址栏,会发现 ID 是会发生变化的。因为我们在createContact()中为每个联系人分配了一个ID号。

我们再次查看路由信息,

javascript 复制代码
...
{
    path: "contacts/:contactId",
    element: <Contact />,
},
  ...

在上节中我们已经详细的说过,通过 :contactId 路由变量,我们可以获取到这个符在地址中的 id 号的。我们再次对 utils.jsx 进行修改:注意里面的备注信息,很重要。

javascript 复制代码
import { getContacts, createContact, getContact } from "./contacts";

// 模拟网络请求,获取数据
export async function rootLoader() {
    const contacts = await getContacts();
    return { contacts };
}

//创建新的联系人
export async function rootAction() {
    const contact = await createContact();
    return { contact };
}

// 根据 URL 中的ID 获取对应的联系人信息。变量 params 是一个对象,包含了 URL 中的参数。
// 其中 contactId 就是路由变量中的 :contactId,即 /contacts/:contactId。同名变量。
export async function contactLoader({ params }) {
    const contact = await getContact(params.contactId);
    return { contact };
}

为了不造成理解上的概念混淆,我们把原先的action 更名为 rootAction, 新增了 contactLoader函数,你应该理解过来了,这个loader是用有 组件的路由配置中的。 然后在 routerConfig.jsx中进行修改。

javascript 复制代码
import { createBrowserRouter } from "react-router-dom";

import Root from "./Root";
import Error404 from "./Error404";
import Contact from "./Contact";
import {
    rootLoader,
    rootAction,
    contactLoader,
} from "./utils";

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

export default router;

现在组件<Contact/> 加载后就能获取到相应的 contact 信息了,我们只要在组件内用 useLoaderData() 就能获取到。对Contact.jsx进行修改:

javascript 复制代码
// Contact.jsx

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


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

}

将之前表态定义的联系人信息删掉,用 上面的替换。 上面我已经注释了。

更新联系人信息

就像创建数据一样,您可以使用 更新数据。创建 EditContact.jsx 文件。

javascript 复制代码
// Edit.jsx

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://reactrouter.com/_docs/tutorial/12.webp"
                    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>
    );
}

增加路由信息,将编辑组件添加到路由中

javascript 复制代码
// routerConfig.jsx

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

import Root from "./Root";
import Error404 from "./Error404";
import Contact from "./Contact";
import EditContact from "./EditContact";
import {
    rootLoader,
    rootAction,
    contactLoader,
} from "./utils";

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

            {
                path: "contacts/:contactId/edit",
                element: <EditContact />,
                loader: contactLoader,
            },
        ]
    }
]);

export default router;

配置在子路由中的目的就是在 Root 中的 <Outlet/>的位置中显示的。

注意路由的配置,当我们点击 Edit 按钮后,注意查看 URL 的变化,如:http://localhost:5173/contacts/uiahrwo/edit, 一眼就能看出可以匹配 ath: "contacts/:contactId/edit"的配置信息。

现在在界面应该如下所示:

现在我人需要将编辑的信息反映到 action 中来更新信息。在 utils.jsx 中创建 editAction(), 并配置到路由中。文件中的参数我已经作了详细的说明。

javascript 复制代码
// utils.jsx

import { redirect } from "react-router-dom";
import { getContacts, createContact, getContact, updateContact } from "./contacts";

...

// 更新联系人信息
// request 是一个对象,包含了请求的所有信息,包括请求头、请求体等。
// params 是一个对象,包含了 URL 中的参数,即路由变量。
// 通过 request.formData() 可以获取到表单数据,返回一个 FormData 对象。
// FormData 对象是一个键值对集合,每个键对应一个值,值可以是字符串,也可以是 Blob 对象。
// 通过 Object.fromEntries() 可以将 FormData 对象转换为一个普通的对象。
// 通过 updateContact() 更新联系人信息。
// redirect() 可以重定向到指定的 URL。
export async function editAction({ request, params }) {
    const formData = await request.formData();
    const updates = Object.fromEntries(formData);
    await updateContact(params.contactId, updates);
    return redirect(`/contacts/${params.contactId}`);
}

更新路由配置信息:

javascript 复制代码
// routerConfig.jsx

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

import Root from "./Root";
import Error404 from "./Error404";
import Contact from "./Contact";
import EditContact from "./EditContact";
import {
    rootLoader,
    rootAction,
    contactLoader,
    editAction,
} from "./utils";

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

            {
                path: "contacts/:contactId/edit",
                element: <EditContact />,
                loader: contactLoader,
                action: editAction
            },
        ]
    }
]);

export default router;

增加活动链接状态

现在我们有一堆记录,不清楚我们在侧边栏中查看的是哪一条。我们可以使用 NavLink 来解决这个问题。

Root 中的 Link 替换为 NavLink ,如下所示:

javascript 复制代码
import {
    Outlet,
    Link,
    useLoaderData,
    Form,
    NavLink,
} from 'react-router-dom';

export default function Root() {
    const { contacts } = useLoaderData();
    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>
                    {contacts.length ? (
                        <ul>
                            {contacts.map((contact) => (
                                <li key={contact.id}>
                                    <NavLink
                                        to={`contacts/${contact.id}`}
                                        className={({ isActive, isPending }) =>
                                            isActive
                                                ? "active"
                                                : isPending
                                                    ? "pending"
                                                    : ""
                                        }
                                    >
                                        {contact.first || contact.last ? (
                                            <>
                                                {contact.first} {contact.last}
                                            </>
                                        ) : (
                                            <i>No Name</i>
                                        )}{" "}
                                        {contact.favorite && <span>★</span>}
                                    </NavLink>
                                </li>
                            ))}
                        </ul>
                    ) : (
                        <p>
                            <i>No contacts</i>
                        </p>
                    )}
                </nav>
            </div>
            <div id="detail"> <Outlet /> </div>
        </>
    );
}

请注意,我们将一个函数传递给 className 。当用户位于 NavLink 中的 URL 时,isActive 将为 true 。当它即将处于活动状态(数据仍在加载)时,isPending将为 true 。这使我们能够轻松指示用户的位置,并就已点击但仍在等待数据加载的链接提供即时反馈。

有时候我们希望数据在加载的时候有个状态反馈,如下图所示:

路由的加载状态可以通过 useNavigation 来获取。我们再次对 Root.jsx 进修修改:

javascript 复制代码
import {
  // 其它代码
   // ... ...

  useNavigation,
} from "react-router-dom";

// ...

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

  return (
    <>
      <div id="sidebar">
      {/* ... */}
      </div>

      <div
        id="detail"
        className={
          navigation.state === "loading" ? "loading" : ""
        }
      >
        <Outlet />
      </div>
    </>
  );
}

由于我们在本地缓存了数据,所以加载速度很快,看不到这个效果,但如果是从网络上执行耗时的操作时,这种效果就很明显了。

删除联系人

现在我们再来完善我们的应用,当我们点击删除按钮后,我们要把 联系人的 ID 传递给路由,再由相应的action去完成删除工作。

查看 Contact.jsx 文件

javascript 复制代码
<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 提交到 destroy 路由,这个路由地址是相对地址。由于Contact 的路由是 contacts/:contactId, 所以这个destroy的路径为 contacts/:contactId/destroy,

我们先创建这个路由的action函数: deleteAction(),

javascript 复制代码
// utils.jsx

import { redirect } from "react-router-dom";
import {
    getContacts,
    createContact,
    getContact,
    updateContact,
    deleteContact
} from "./contacts";

...

// 删除联系人
export async function destroyAction({ params }) {
    await deleteContact(params.contactId);
    return redirect("/");
}

并将它配置到路由中

javascript 复制代码
 ... 

import { action as destroyAction } from "./routes/destroy";

const router = createBrowserRouter([
  {
    path: "/",

    ...

    children: [
       ...

      {
        path: "contacts/:contactId/destroy",
        action: destroyAction,
      },
    ],
  },
]);

...

由于我们没有在子路由中配置 errorElement ,这是因为我们在destroyAction中直接重定向到 " / ", 所以这个element就没有创建的必要了。事实上也的确没有这个需求

javascript 复制代码
 // routerConfig.jsx

[
  ...

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

还有一点非常重要,Form 的 提交方法为 post 时才能激活路由中的action操作。

索引路由

每当我们加载这个App时,你会发现右侧是一个空页面。就像下面这样:

当一条路由有子路由时,而当前页面又处在父路由层级时,<Outlet>没有任何子路由与之匹配,所以就没有可渲染的界面。这个时候可以将索引路由视为填充该空间的默认子路由。

创建 Index 组件如下所示

javascript 复制代码
// Index.jsx

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到路由

javascript 复制代码
// routerConfig.jsx

...

import Index from "./Index";

...

const router = createBrowserRouter([
    {
        path: "/",
        element: <Root />,
        errorElement: <Error404 />,
        loader: rootLoader,
        action: rootAction,
        children: [
            { index: true, element: <Index /> },
            
            ...
        ]
    }
]);

export default router;

这个时候我们进入App重新渲染后界面如下:

完美。

现在还有最后一个功能没有完成,就是搜索功能,我们希望根据输入的关键词来搜索出联系人。

URL的搜索参数与Get提交

传统的html表单中,如果没有指定提交方法的话默认为get方式提交到服务器,如我们Root.jsx中搜索框部分,这自然不是我们想要的结果,我们只是想把输入参数反应到地址栏中而不影响浏览器的变化,正好,Form 可以做到。我们把Root中的 html 元素 form 改成 React Router 中的 Form组件就好了,就像下面这样:

javascript 复制代码
// Root.jsx
...
<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>
...

现在你的搜索栏中输入些内容回车后,你会发现浏览器的地址栏信息也会发会变化,但浏览器并没有网络请求操作。这正是我们目的,

现在我们修改 rootLoader()函数, 以获取get参数,并根据这个参数做出筛选联系人的反应:

javascript 复制代码
// utils.jsx

...

// 模拟网络请求,获取数据
export async function rootLoader({request}) {
    const url = new URL(request.url);
    const q = url.searchParams.get("q");
    const contacts = await getContacts(q);
    return { contacts };
}
...

现在就很美了。

再次强调:因为这是一个 get 的,而不是 postReact Router 路由器不调用 action 。提交 GET 表格与单击链接相同:只有URL更改。这就是为什么我们添加的过滤代码在loader中,而不是在 action 中。

这也意味着这是一个普通的页面导航。您可以单击"后退"按钮以返回到自己的位置。

最后

关于路由的主要应用技术我已经讲完了,根据这些应用方法,配置 React的状态管理,会使我们的应用更加灵活,设计更加方便。只要多练习,多思考,就一定能开发出非常完美的产品。

相关推荐
xjt_090114 分钟前
基于 Vue 3 构建企业级 Web Components 组件库
前端·javascript·vue.js
我是伪码农26 分钟前
Vue 2.3
前端·javascript·vue.js
夜郎king1 小时前
HTML5 SVG 实现日出日落动画与实时天气可视化
前端·html5·svg 日出日落
辰风沐阳1 小时前
JavaScript 的宏任务和微任务
javascript
夏幻灵2 小时前
HTML5里最常用的十大标签
前端·html·html5
冰暮流星2 小时前
javascript之二重循环练习
开发语言·javascript·数据库
Mr Xu_2 小时前
Vue 3 中 watch 的使用详解:监听响应式数据变化的利器
前端·javascript·vue.js
未来龙皇小蓝2 小时前
RBAC前端架构-01:项目初始化
前端·架构
程序员agions2 小时前
2026年,微前端终于“死“了
前端·状态模式
万岳科技系统开发2 小时前
食堂采购系统源码库存扣减算法与并发控制实现详解
java·前端·数据库·算法