Django5+React18前后端分离开发实战14 React-Router6 入门教程

使用nodejs18

首先,将nodejs切换到18版本:

nvm use 18

在这里插入图片描述

创建项目

npm create vite@latest zdpreact_basic_router_dev -- --template react
cd zdpreact_basic_router_dev 
npm install react-router-dom localforage match-sorter sort-by
npm run dev

此时访问:http://localhost:5173/

在这里插入图片描述

使用webstorm打开项目

使用webstorm打开项目,然后配置一个npm启动:
在这里插入图片描述

接着,通过快捷方式启动服务:
在这里插入图片描述

基础环境

修改 src/index.css

html {
  box-sizing: border-box;
}
*,
*:before,
*:after {
  box-sizing: inherit;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
  "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
  sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
  monospace;
}

html,
body {
  height: 100%;
  margin: 0;
  line-height: 1.5;
  color: #121212;
}
textarea,
input,
button {
  font-size: 1rem;
  font-family: inherit;
  border: none;
  border-radius: 8px;
  padding: 0.5rem 0.75rem;
  box-shadow: 0 0px 1px hsla(0, 0%, 0%, 0.2), 0 1px 2px hsla(0, 0%, 0%, 0.2);
  background-color: white;
  line-height: 1.5;
  margin: 0;
}
button {
  color: #3992ff;
  font-weight: 500;
}

textarea:hover,
input:hover,
button:hover {
  box-shadow: 0 0px 1px hsla(0, 0%, 0%, 0.6), 0 1px 2px hsla(0, 0%, 0%, 0.2);
}

button:active {
  box-shadow: 0 0px 1px hsla(0, 0%, 0%, 0.4);
  transform: translateY(1px);
}

#contact h1 {
  display: flex;
  align-items: flex-start;
  gap: 1rem;
}
#contact h1 form {
  display: flex;
  align-items: center;
  margin-top: 0.25rem;
}
#contact h1 form button {
  box-shadow: none;
  font-size: 1.5rem;
  font-weight: 400;
  padding: 0;
}
#contact h1 form button[value="true"] {
  color: #a4a4a4;
}
#contact h1 form button[value="true"]:hover,
#contact h1 form button[value="false"] {
  color: #eeb004;
}

form[action$="destroy"] button {
  color: #f44250;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}

#root {
  display: flex;
  height: 100%;
  width: 100%;
}

#sidebar {
  width: 22rem;
  background-color: #f7f7f7;
  border-right: solid 1px #e3e3e3;
  display: flex;
  flex-direction: column;
}

#sidebar > * {
  padding-left: 2rem;
  padding-right: 2rem;
}

#sidebar h1 {
  font-size: 1rem;
  font-weight: 500;
  display: flex;
  align-items: center;
  margin: 0;
  padding: 1rem 2rem;
  border-top: 1px solid #e3e3e3;
  order: 1;
  line-height: 1;
}

#sidebar h1::before {
  content: url("data:image/svg+xml,%3Csvg width='25' height='18' viewBox='0 0 25 18' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M19.4127 6.4904C18.6984 6.26581 18.3295 6.34153 17.5802 6.25965C16.4219 6.13331 15.9604 5.68062 15.7646 4.51554C15.6551 3.86516 15.7844 2.9129 15.5048 2.32334C14.9699 1.19921 13.7183 0.695046 12.461 0.982805C11.3994 1.22611 10.516 2.28708 10.4671 3.37612C10.4112 4.61957 11.1197 5.68054 12.3363 6.04667C12.9143 6.22097 13.5284 6.3087 14.132 6.35315C15.2391 6.43386 15.3241 7.04923 15.6236 7.55574C15.8124 7.87508 15.9954 8.18975 15.9954 9.14193C15.9954 10.0941 15.8112 10.4088 15.6236 10.7281C15.3241 11.2334 14.9547 11.5645 13.8477 11.6464C13.244 11.6908 12.6288 11.7786 12.0519 11.9528C10.8353 12.3201 10.1268 13.3799 10.1828 14.6234C10.2317 15.7124 11.115 16.7734 12.1766 17.0167C13.434 17.3056 14.6855 16.8003 15.2204 15.6762C15.5013 15.0866 15.6551 14.4187 15.7646 13.7683C15.9616 12.6032 16.423 12.1505 17.5802 12.0242C18.3295 11.9423 19.1049 12.0242 19.8071 11.6253C20.5491 11.0832 21.212 10.2696 21.212 9.14192C21.212 8.01428 20.4976 6.83197 19.4127 6.4904Z' fill='%23F44250'/%3E%3Cpath d='M7.59953 11.7459C6.12615 11.7459 4.92432 10.5547 4.92432 9.09441C4.92432 7.63407 6.12615 6.44287 7.59953 6.44287C9.0729 6.44287 10.2747 7.63407 10.2747 9.09441C10.2747 10.5536 9.07172 11.7459 7.59953 11.7459Z' fill='black'/%3E%3Cpath d='M2.64217 17.0965C1.18419 17.093 -0.0034949 15.8971 7.72743e-06 14.4356C0.00352588 12.9765 1.1994 11.7888 2.66089 11.7935C4.12004 11.797 5.30772 12.9929 5.30306 14.4544C5.29953 15.9123 4.10366 17.1 2.64217 17.0965Z' fill='black'/%3E%3Cpath d='M22.3677 17.0965C20.9051 17.1046 19.7046 15.9217 19.6963 14.4649C19.6882 13.0023 20.8712 11.8017 22.3279 11.7935C23.7906 11.7854 24.9911 12.9683 24.9993 14.4251C25.0075 15.8866 23.8245 17.0883 22.3677 17.0965Z' fill='black'/%3E%3C/svg%3E%0A");
  margin-right: 0.5rem;
  position: relative;
  top: 1px;
}

#sidebar > div {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding-top: 1rem;
  padding-bottom: 1rem;
  border-bottom: 1px solid #e3e3e3;
}

#sidebar > div form {
  position: relative;
}

#sidebar > div form input[type="search"] {
  width: 100%;
  padding-left: 2rem;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='h-6 w-6' fill='none' viewBox='0 0 24 24' stroke='%23999' stroke-width='2'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z' /%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: 0.625rem 0.75rem;
  background-size: 1rem;
  position: relative;
}

#sidebar > div form input[type="search"].loading {
  background-image: none;
}

#search-spinner {
  width: 1rem;
  height: 1rem;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24'%3E%3Cpath stroke='%23000' strokeLinecap='round' strokeLinejoin='round' strokeWidth='2' d='M20 4v5h-.582m0 0a8.001 8.001 0 00-15.356 2m15.356-2H15M4 20v-5h.581m0 0a8.003 8.003 0 0015.357-2M4.581 15H9' /%3E%3C/svg%3E");
  animation: spin 1s infinite linear;
  position: absolute;
  left: 0.625rem;
  top: 0.75rem;
}

@keyframes spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

#sidebar nav {
  flex: 1;
  overflow: auto;
  padding-top: 1rem;
}

#sidebar nav a span {
  float: right;
  color: #eeb004;
}
#sidebar nav a.active span {
  color: inherit;
}

i {
  color: #818181;
}
#sidebar nav .active i {
  color: inherit;
}

#sidebar ul {
  padding: 0;
  margin: 0;
  list-style: none;
}

#sidebar li {
  margin: 0.25rem 0;
}

#sidebar nav a {
  display: flex;
  align-items: center;
  justify-content: space-between;
  overflow: hidden;

  white-space: pre;
  padding: 0.5rem;
  border-radius: 8px;
  color: inherit;
  text-decoration: none;
  gap: 1rem;
}

#sidebar nav a:hover {
  background: #e3e3e3;
}

#sidebar nav a.active {
  background: hsl(224, 98%, 58%);
  color: white;
}

#sidebar nav a.pending {
  color: hsl(224, 98%, 58%);
}

#detail {
  flex: 1;
  padding: 2rem 4rem;
  width: 100%;
}

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

#contact {
  max-width: 40rem;
  display: flex;
}

#contact h1 {
  font-size: 2rem;
  font-weight: 700;
  margin: 0;
  line-height: 1.2;
}

#contact h1 + p {
  margin: 0;
}

#contact h1 + p + p {
  white-space: break-spaces;
}

#contact h1:focus {
  outline: none;
  color: hsl(224, 98%, 58%);
}

#contact a[href*="twitter"] {
  display: flex;
  font-size: 1.5rem;
  color: #3992ff;
  text-decoration: none;
}
#contact a[href*="twitter"]:hover {
  text-decoration: underline;
}

#contact img {
  width: 12rem;
  height: 12rem;
  background: #c8c8c8;
  margin-right: 2rem;
  border-radius: 1.5rem;
  object-fit: cover;
}

#contact h1 ~ div {
  display: flex;
  gap: 0.5rem;
  margin: 1rem 0;
}

#contact-form {
  display: flex;
  max-width: 40rem;
  flex-direction: column;
  gap: 1rem;
}
#contact-form > p:first-child {
  margin: 0;
  padding: 0;
}
#contact-form > p:first-child > :nth-child(2) {
  margin-right: 1rem;
}
#contact-form > p:first-child,
#contact-form label {
  display: flex;
}
#contact-form p:first-child span,
#contact-form label span {
  width: 8rem;
}
#contact-form p:first-child input,
#contact-form label input,
#contact-form label textarea {
  flex-grow: 2;
}

#contact-form-avatar {
  margin-right: 2rem;
}

#contact-form-avatar img {
  width: 12rem;
  height: 12rem;
  background: hsla(0, 0%, 0%, 0.2);
  border-radius: 1rem;
}

#contact-form-avatar input {
  box-sizing: border-box;
  width: 100%;
}

#contact-form p:last-child {
  display: flex;
  gap: 0.5rem;
  margin: 0 0 0 8rem;
}

#contact-form p:last-child button[type="button"] {
  color: inherit;
}

#zero-state {
  margin: 2rem auto;
  text-align: center;
  color: #818181;
}

#zero-state a {
  color: inherit;
}

#zero-state a:hover {
  color: #121212;
}

#zero-state:before {
  display: block;
  margin-bottom: 0.5rem;
  content: url("data:image/svg+xml,%3Csvg width='50' height='33' viewBox='0 0 50 33' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M38.8262 11.1744C37.3975 10.7252 36.6597 10.8766 35.1611 10.7128C32.8444 10.4602 31.9215 9.55475 31.5299 7.22456C31.3108 5.92377 31.5695 4.01923 31.0102 2.8401C29.9404 0.591789 27.4373 -0.416556 24.9225 0.158973C22.7992 0.645599 21.0326 2.76757 20.9347 4.94569C20.8228 7.43263 22.2399 9.5546 24.6731 10.2869C25.8291 10.6355 27.0574 10.8109 28.2646 10.8998C30.4788 11.0613 30.6489 12.292 31.2479 13.3051C31.6255 13.9438 31.9914 14.5731 31.9914 16.4775C31.9914 18.3819 31.6231 19.0112 31.2479 19.6499C30.6489 20.6606 29.9101 21.3227 27.696 21.4865C26.4887 21.5754 25.2581 21.7508 24.1044 22.0994C21.6712 22.834 20.2542 24.9537 20.366 27.4406C20.4639 29.6187 22.2306 31.7407 24.3538 32.2273C26.8686 32.8052 29.3717 31.7945 30.4415 29.5462C31.0032 28.3671 31.3108 27.0312 31.5299 25.7304C31.9238 23.4002 32.8467 22.4948 35.1611 22.2421C36.6597 22.0784 38.2107 22.2421 39.615 21.4443C41.099 20.36 42.4248 18.7328 42.4248 16.4775C42.4248 14.2222 40.9961 11.8575 38.8262 11.1744Z' fill='%23E3E3E3'/%3E%3Cpath d='M15.1991 21.6854C12.2523 21.6854 9.84863 19.303 9.84863 16.3823C9.84863 13.4615 12.2523 11.0791 15.1991 11.0791C18.1459 11.0791 20.5497 13.4615 20.5497 16.3823C20.5497 19.3006 18.1436 21.6854 15.1991 21.6854Z' fill='%23E3E3E3'/%3E%3Cpath d='M5.28442 32.3871C2.36841 32.38 -0.00698992 29.9882 1.54551e-05 27.0652C0.00705187 24.1469 2.39884 21.7715 5.32187 21.7808C8.24022 21.7878 10.6156 24.1796 10.6063 27.1027C10.5992 30.0187 8.20746 32.3941 5.28442 32.3871Z' fill='%23E3E3E3'/%3E%3Cpath d='M44.736 32.387C41.8107 32.4033 39.4096 30.0373 39.3932 27.1237C39.3769 24.1984 41.7428 21.7973 44.6564 21.7808C47.5817 21.7645 49.9828 24.1305 49.9993 27.0441C50.0156 29.9671 47.6496 32.3705 44.736 32.387Z' fill='%23E3E3E3'/%3E%3C/svg%3E%0A");
}

#error-page {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 100%;
}

新增 src/contacts.js

import localforage from "localforage";
import { matchSorter } from "match-sorter";
import sortBy from "sort-by";

export async function getContacts(query) {
    await fakeNetwork(`getContacts:${query}`);
    let contacts = await localforage.getItem("contacts");
    if (!contacts) contacts = [];
    if (query) {
        contacts = matchSorter(contacts, query, { keys: ["first", "last"] });
    }
    return contacts.sort(sortBy("last", "createdAt"));
}

export async function createContact() {
    await fakeNetwork();
    let id = Math.random().toString(36).substring(2, 9);
    let contact = { id, createdAt: Date.now() };
    let contacts = await getContacts();
    contacts.unshift(contact);
    await set(contacts);
    return contact;
}

export async function getContact(id) {
    await fakeNetwork(`contact:${id}`);
    let contacts = await localforage.getItem("contacts");
    let contact = contacts.find(contact => contact.id === id);
    return contact ?? null;
}

export async function updateContact(id, updates) {
    await fakeNetwork();
    let contacts = await localforage.getItem("contacts");
    let contact = contacts.find(contact => contact.id === id);
    if (!contact) throw new Error("No contact found for", id);
    Object.assign(contact, updates);
    await set(contacts);
    return contact;
}

export async function deleteContact(id) {
    let contacts = await localforage.getItem("contacts");
    let index = contacts.findIndex(contact => contact.id === id);
    if (index > -1) {
        contacts.splice(index, 1);
        await set(contacts);
        return true;
    }
    return false;
}

function set(contacts) {
    return localforage.setItem("contacts", contacts);
}

// fake a cache so we don't slow down stuff we've already seen
let fakeCache = {};

async function fakeNetwork(key) {
    if (!key) {
        fakeCache = {};
    }

    if (fakeCache[key]) {
        return;
    }

    fakeCache[key] = true;
    return new Promise(res => {
        setTimeout(res, Math.random() * 800);
    });
}

修改 src/main.jsx

import * as React from "react";
import * as ReactDOM from "react-dom/client";
import {
    createBrowserRouter,
    RouterProvider,
} 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>
);

重新预览

http://localhost:5173/

在这里插入图片描述

根路由

新增 src/routes/root.jsx

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>
        </>
    );
}

修改 src/main.jsx

import * as React from "react";
import * as ReactDOM from "react-dom/client";
import {
    createBrowserRouter,
    RouterProvider,
} from "react-router-dom";
import "./index.css";
import Root from "./routes/root.jsx"

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

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

重新预览

http://localhost:5173/

在这里插入图片描述

处理404错误

新增 src/error-page.jsx

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>
    );
}

修改 src/main.jsx

import * as React from "react";
import * as ReactDOM from "react-dom/client";
import {
    createBrowserRouter,
    RouterProvider,
} from "react-router-dom";
import "./index.css";
import Root from "./routes/root.jsx"
import ErrorPage from "./error-page.jsx";

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

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

访问不存在页面

此时,访问:http://localhost:5173/404

在这里插入图片描述

联系人页面

新增 src/routes/contact.jsx

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>
    );
}

修改 src/main.jsx

import * as React from "react";
import * as ReactDOM from "react-dom/client";
import {
    createBrowserRouter,
    RouterProvider,
} from "react-router-dom";
import "./index.css";
import Root from "./routes/root.jsx"
import ErrorPage from "./error-page.jsx";
import Contact from "./routes/contact.jsx";

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

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

访问联系人页面

浏览器访问:http://localhost:5173/contacts/1

在这里插入图片描述

嵌套路由

修改 src/main.jsx

import * as React from "react";
import * as ReactDOM from "react-dom/client";
import {
    createBrowserRouter,
    RouterProvider,
} from "react-router-dom";
import "./index.css";
import Root from "./routes/root.jsx"
import ErrorPage from "./error-page.jsx";
import Contact from "./routes/contact.jsx";

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

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

修改 src/routes/root.jsx

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

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">
                {/*嵌套路由*/}
                <Outlet/>
            </div>
        </>
    );
}

继续修改 src/routes/root.jx

修改左侧菜单链接:

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

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>
                            {/*自动跳转路由*/}
                            <Link to={`contacts/1`}>Your Name</Link>
                        </li>
                        <li>
                            <Link to={`contacts/2`}>Your Friend</Link>
                        </li>
                    </ul>
                </nav>
            </div>
            <div id="detail">
                {/*嵌套路由*/}
                <Outlet/>
            </div>
        </>
    );
}

效果预览

先访问首页,在点击左侧菜单的链接,会自动跳转:http://localhost:5173/contacts/1

在这里插入图片描述

加载数据

修改 src/routes/root.jsx

核心代码:

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

// 加载联系人信息
export async function loader() {
    const contacts = await getContacts();
    return {contacts};
}

完整代码:

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

// 加载联系人信息
export async function loader() {
    const contacts = await getContacts();
    return {contacts};
}

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>
                            {/*自动跳转路由*/}
                            <Link to={`contacts/1`}>Your Name</Link>
                        </li>
                        <li>
                            <Link to={`contacts/2`}>Your Friend</Link>
                        </li>
                    </ul>
                </nav>
            </div>
            <div id="detail">
                {/*嵌套路由*/}
                <Outlet/>
            </div>
        </>
    );
}

修改 src/main.jsx

核心代码:

import Root, {loader as rootLoader} from "./routes/root.jsx"

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

完整代码:

import * as React from "react";
import * as ReactDOM from "react-dom/client";
import {
    createBrowserRouter,
    RouterProvider,
} from "react-router-dom";
import "./index.css";
import Root, {loader as rootLoader} from "./routes/root.jsx"
import ErrorPage from "./error-page.jsx";
import Contact from "./routes/contact.jsx";

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

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

再次修改 src/routes/root.jsx

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

// 加载联系人信息
export async function loader() {
    const contacts = await getContacts();
    return {contacts};
}

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}>
                                    <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>
            </div>
            <div id="detail">
                {/*嵌套路由*/}
                <Outlet/>
            </div>
        </>
    );
}

新增联系人功能

修改 src/routes/root.jsx

核心代码:

export async function action() {
    const contact = await createContact();
    return {contact};
}

完整代码:

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

// 加载联系人信息
export async function loader() {
    const contacts = await getContacts();
    return {contacts};
}

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

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}>
                                    <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>
            </div>
            <div id="detail">
                {/*嵌套路由*/}
                <Outlet/>
            </div>
        </>
    );
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Python-3.12.0文档解读-内置函数ord()详细说明+记忆策略+常用场景+巧妙用法+综合技巧

一个认为一切根源都是“自己不够强”的INTJ 个人主页&#xff1a;用哲学编程-CSDN博客专栏&#xff1a;每日一题——举一反三Python编程学习Python内置函数 Python-3.12.0文档解读 目录 详细说明 概述 语法 参数 返回值 示例 注意事项 应用场景 记忆策略 常用场景…

JD3-40/23漏电继电器 AC220V 50-500mA 0.1s 导轨安装

系列型号&#xff1a; JD3-40/13漏电继电器JD3-40/23漏电继电器JD3-40/33漏电继电器JD3-40/43漏电继电器 JD3-70/13漏电继电器JD3-70/23漏电继电器JD3-70/33漏电继电器JD3-70/43漏电继电器 JD3-100/23漏电继电器JD3-100/43漏电继电器JD3-100/33漏电继电器JD3-100/13漏电继电…

ARM架构与分类

ARM架构&#xff0c;曾称进阶精简指令集机器&#xff08;Advanced RISC Machine&#xff09;更早称作Acorn RISC Machine&#xff0c;是一个32位精简指令集&#xff08;RISC&#xff09;处理器架构。 主要是根据FPGA zynq-7000的芯片编写的知识思维导图总结&#xff0c;没有会…

【NumPy】全面解析NumPy随机数生成器:使用numpy.random的实用技巧

&#x1f9d1; 博主简介&#xff1a;阿里巴巴嵌入式技术专家&#xff0c;深耕嵌入式人工智能领域&#xff0c;具备多年的嵌入式硬件产品研发管理经验。 &#x1f4d2; 博客介绍&#xff1a;分享嵌入式开发领域的相关知识、经验、思考和感悟&#xff0c;欢迎关注。提供嵌入式方向…

【软件设计师】算法

1、算法的效率 时间复杂度:程序从开始到结束所需要的时间 空间复杂度&#xff1a;算法在运行过程中临时占用存储空间大小的度量 时间渐近复杂度&#xff1a;时间复杂度由最高次幂决定(判断大小技巧&#xff1a;将n10代入&#xff09; O(log2 n):二分查找法 O(n&#xff09;:for…

720VR三维立体小程序源码系统 手机电脑端自适应 前后端分离 带完整的安装代码包以及搭建教程

系统概述 720VR 三维立体小程序源码系统是基于先进的技术和理念打造而成的综合性平台。它融合了虚拟现实技术、移动互联网技术以及计算机编程技术&#xff0c;旨在为用户提供沉浸式的 720 度全景体验。 该系统的设计充分考虑了用户的需求&#xff0c;无论是在手机端还是电脑端…

从零学爬虫:使用比如说说解析网页结构

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一、引言 二、网页结构概述 示例&#xff1a;查看网页结构 三、使用比如说说解析网页 1.…

抖音小程序如何生成二维码

1.页面结构 <image src{{imgUrl}}></image>2.代码结构 onLoad(options) {if (options.param) {var qrCode 13246897451257 //传入生成二维码的字符串this.generateQRCode(qrCode);}},//调起第三方库qrCodegenerateQRCode(text) {//调用了qrCode里面的apiconst api…

uniapp App去除iOS底部安全区域白边

未设置的情况下&#xff0c;iOS底部安全区域白边 如图&#xff1a; 去除方法&#xff1a; 在 mainfest.json 中加入一下代码&#xff1a; "safearea" : {"bottom" : {"offset" : "none"} }, 去除效果展示&#xff1a;

HTTP请求拦截器链

文章目录 HTTP请求拦截器链需求定义写一个Controller方法接口写三个http请求拦截器把拦截器加入到配置中&#xff0c;并且配置拦截规则在postman里面发送请求&#xff0c;看下测试结果是否正确 HTTP请求拦截器链 需求定义 我们写一个包含三个HTTP请求拦截器的拦截器链&#x…

盲人无障碍设施建设:科技之光照亮前行之路

在这个快速发展的时代&#xff0c;科技的每一次进步都在悄然改变着我们的生活&#xff0c;尤其在提升特殊群体生活质量方面&#xff0c;展现出前所未有的力量。今天&#xff0c;让我们聚焦于盲人无障碍设施建设这一重要话题&#xff0c;通过一款名为“蝙蝠避障”的辅助软件&…

饲料粉碎混合机组:打造精细化养殖

饲料粉碎混合机组是畜牧业和养殖业中不可或缺的设备。它集饲料粉碎和混合于一体&#xff0c;可以高效地处理各种饲料原料&#xff0c;提高饲料的均匀度和营养价值。 具体来说&#xff0c;饲料粉碎混合机组的主要功能包括将饲料原料进行粉碎&#xff0c;增加其表面积和调质粒度…

小程序唯品会Authorization sign

声明 本文章中所有内容仅供学习交流使用&#xff0c;不用于其他任何目的&#xff0c;抓包内容、敏感网址、数据接口等均已做脱敏处理&#xff0c;严禁用于商业用途和非法用途&#xff0c;否则由此产生的一切后果均与作者无关&#xff01;wx a15018601872 本文章未…

13.Redis之数据库管理redis客户端JAVA客户端

1.数据库管理 mysql 中有一个重要的概念,database 1个 mysql 服务器上可以有很多个 database1个 database 上可以有很多个 表mysql 上可以随心所欲的 创建/删除 数据库~~ Redis 提供了⼏个⾯向 Redis 数据库的操作&#xff0c;分别是 dbsize、select、flushdb、flushall 命令…

Web漏洞:网络安全的隐形杀手

随着互联网的深入发展&#xff0c;Web应用程序已成为企业和个人生活中不可或缺的一部分。然而&#xff0c;随着Web应用的普及&#xff0c;网络安全问题也日益凸显&#xff0c;其中Web漏洞是网络安全领域面临的重大挑战之一。本文将详细探讨一些常见的Web漏洞及其潜在的危害&…

期望薪资25K,新浪微博测试4轮面试,没想到过了。。

一面60min 1、离职原因 2、简单的算法题&#xff0c;就是我会什么让写什么&#xff1a; 冒泡排序&#xff0c;二分查找&#xff08;其实这么简单&#xff0c;我还是在指引下写出来的&#xff0c;自己实在太菜&#xff09; 3、简历问答&#xff08;随机抽几个点问&#xff0…

弘君资本:沪指跌0.46%,电力板块逆市爆发,半导体板块强势

28日&#xff0c;沪指早盘窄幅震动&#xff0c;午后回落走低&#xff1b;深证成指、创业板指大幅下探&#xff1b;两市成交额小幅萎缩。 截至收盘&#xff0c;沪指跌0.46%报3109.57点&#xff0c;深证成指跌1.23%报9391.05点&#xff0c;创业板指跌1.35%报1806.25点&#xff0c…

AutoDL搭建 ChatGLM3

租用新实例 这里选择的西北 B 区、RTX 409024GB 创建虚拟环境并激活 # 安装虚拟环境至数据盘 conda create --prefix /root/autodl-tmp/envs/chatglm3-demo python3.10# 激活虚拟环境 conda activate /root/autodl-tmp/envs/chatglm3-demo拉取ChatGLM3仓库代码 # 开启学术…

无人机+EasyDSS互联网视频平台:构建秸秆焚烧监控的“天眼”系统

一、方案背景 在每年的夏收时节&#xff0c;秸秆禁烧成为各地政府面临的一项重要任务。随着夏收季节的结束&#xff0c;大量农作物秸秆的处理问题逐渐凸显。一方面农作物种植面积辽阔&#xff0c;禁烧区域面积较大&#xff0c;监管巡逻人员的数量有限&#xff0c;无法全面顾及…

解锁数据奥秘,SPSS for Mac/WIN助您智赢未来

在信息爆炸的时代&#xff0c;数据已成为推动社会进步和企业发展的核心动力。但如何将这些海量数据转化为有价值的洞见&#xff0c;却是摆在每一位决策者面前的难题。IBM SPSS Statistics&#xff0c;一款专业的统计分析软件&#xff0c;凭借其强大的功能和易用的界面&#xff…