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的状态管理,会使我们的应用更加灵活,设计更加方便。只要多练习,多思考,就一定能开发出非常完美的产品。

相关推荐
Summer不秃4 分钟前
Flutter之使用mqtt进行连接和信息传输的使用案例
前端·flutter
旭日猎鹰8 分钟前
Flutter踩坑记录(二)-- GestureDetector+Expanded点击无效果
前端·javascript·flutter
Viktor_Ye15 分钟前
高效集成易快报与金蝶应付单的方案
java·前端·数据库
hummhumm17 分钟前
第 25 章 - Golang 项目结构
java·开发语言·前端·后端·python·elasticsearch·golang
乐闻x43 分钟前
Vue.js 性能优化指南:掌握 keep-alive 的使用技巧
前端·vue.js·性能优化
一条晒干的咸魚1 小时前
【Web前端】创建我的第一个 Web 表单
服务器·前端·javascript·json·对象·表单
花海少爷1 小时前
第十章 JavaScript的应用课后习题
开发语言·javascript·ecmascript
Amd7941 小时前
Nuxt.js 应用中的 webpack:compiled 事件钩子
前端·webpack·开发·编译·nuxt.js·事件·钩子
生椰拿铁You1 小时前
09 —— Webpack搭建开发环境
前端·webpack·node.js
狸克先生1 小时前
如何用AI写小说(二):Gradio 超简单的网页前端交互
前端·人工智能·chatgpt·交互