npm install react-router-dom@6 history@5
import * as React from "react";
import * as ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById("root")
);
import { Link } from "react-router-dom";
export default function App() {
return (
<div>
<h1>Bookkeeper</h1>
<nav
style={{
borderBottom: "solid 1px",
paddingBottom: "1rem"
}}
>
<Link to="/invoices">Invoices</Link> |{" "}
<Link to="/expenses">Expenses</Link>
</nav>
</div>
);
}
import { render } from "react-dom";
import {
BrowserRouter,
Routes,
Route
} from "react-router-dom";
import App from "./App";
import Expenses from "./routes/expenses";
import Invoices from "./routes/invoices";
const rootElement = document.getElementById("root");
render(
<BrowserRouter>
<Routes>
<Route path="/" element={<App />} />
<Route path="expenses" element={<Expenses />} />
<Route path="invoices" element={<Invoices />} />
</Routes>
</BrowserRouter>,
rootElement
);
we need to render an Outlet
in the App
route:
import { Outlet, Link } from "react-router-dom";
export default function App() {
return (
<div>
<h1>Bookkeeper</h1>
<nav
style={{
borderBottom: "solid 1px",
paddingBottom: "1rem"
}}
>
<Link to="/invoices">Invoices</Link> |{" "}
<Link to="/expenses">Expenses</Link>
</nav>
<Outlet />
</div>
);
}
It nests the URLs ("/" + "expenses" and "/" + "invoices")
import { render } from "react-dom";
import {
BrowserRouter,
Routes,
Route
} from "react-router-dom";
import App from "./App";
import Expenses from "./routes/expenses";
import Invoices from "./routes/invoices";
const rootElement = document.getElementById("root");
render(
<BrowserRouter>
<Routes>
<Route path="/" element={<App />}>
<Route path="expenses" element={<Expenses />} />
<Route path="invoices" element={<Invoices />} />
</Route>
</Routes>
</BrowserRouter>,
rootElement
);
let invoices = [
{
name: "Santa Monica",
number: 1995,
amount: "$10,800",
due: "12/05/1995"
},
{
name: "Stankonia",
number: 2000,
amount: "$8,000",
due: "10/31/2000"
},
{
name: "Ocean Avenue",
number: 2003,
amount: "$9,500",
due: "07/22/2003"
},
{
name: "Tubthumper",
number: 1997,
amount: "$14,000",
due: "09/01/1997"
},
{
name: "Wide Open Spaces",
number: 1998,
amount: "$4,600",
due: "01/27/2998"
}
];
export function getInvoices() {
return invoices;
}
import { Link } from "react-router-dom";
import { getInvoices } from "../data";
export default function Invoices() {
let invoices = getInvoices();
return (
<div style={{ display: "flex" }}>
<nav
style={{
borderRight: "solid 1px",
padding: "1rem"
}}
>
{invoices.map(invoice => (
<Link
style={{ display: "block", margin: "1rem 0" }}
to={`/invoices/${invoice.number}`}
key={invoice.number}
>
{invoice.name}
</Link>
))}
</nav>
</div>
);
}
<Routes>
<Route path="/" element={<App />}>
<Route path="expenses" element={<Expenses />} />
<Route path="invoices" element={<Invoices />} />
<Route
path="*"
element={
<main style={{ padding: "1rem" }}>
<p>There's nothing here!</p>
</main>
}
/>
</Route>
</Routes>
The *
has special meaning here. It will match only when no other routes do.
We just visited some URLs like /invoices/1998
and /invoices/2005
<Routes>
<Route path="/" element={<App />}>
<Route path="expenses" element={<Expenses />} />
<Route path="invoices" element={<Invoices />}>
<Route path=":invoiceId" element={<Invoice />} />
</Route>
<Route
path="*"
element={
<main style={{ padding: "1rem" }}>
<p>There's nothing here!</p>
</main>
}
/>
</Route>
</Routes>
The Route
adds a second layer of route nesting when it matches, Because the Route
is nested the UI will be nested too.
use :invoiceId
WITH :invoiceId -> params.invoiceId
import { useParams } from "react-router-dom";
export default function Invoice() {
let params = useParams();
return <h2>Invoice: {params.invoiceId}</h2>;
}
Click on the Invoices
link in the global nav of your app. Notice that the main content area goes blank! We can fix this with an index
route.
<Routes>
<Route path="/" element={<App />}>
<Route path="expenses" element={<Expenses />} />
<Route path="invoices" element={<Invoices />}>
<Route
index
element={
<main style={{ padding: "1rem" }}>
<p>Select an invoice</p>
</main>
}
/>
<Route path=":invoiceId" element={<Invoice />} />
</Route>
<Route
path="*"
element={
<main style={{ padding: "1rem" }}>
<p>There's nothing here!</p>
</main>
}
/>
</Route>
</Routes>
Notice it has the index
prop instead of a path
. That's because the index route shares the path of the parent. That's the whole point--it doesn't have a path.
the active link the user is looking at
import { NavLink, Outlet } from "react-router-dom";
import { getInvoices } from "../data";
export default function Invoices() {
let invoices = getInvoices();
return (
<div style={{ display: "flex" }}>
<nav
style={{
borderRight: "solid 1px",
padding: "1rem"
}}
>
{invoices.map(invoice => (
<NavLink
style={({ isActive }) => {
return {
display: "block",
margin: "1rem 0",
color: isActive ? "red" : ""
};
}}
to={`/invoices/${invoice.number}`}
key={invoice.number}
>
{invoice.name}
</NavLink>
))}
</nav>
<Outlet />
</div>
);
}
We did three things there:
- We swapped out Link for NavLink.
- We changed the style from a simple object to a function that returns an object.
- We changed the color of our link by looking at the isActive value that NavLink passed to our styling function.
You can do the same thing with className
on NavLink
:
// normal string
<NavLink className="red" />
// function
<NavLink className={({ isActive }) => isActive ? "red" : "blue"} />
使用useSearchParams
来处理/shoes?brand=nike&sort=asc&sortby=price
import {
NavLink,
Outlet,
useSearchParams
} from "react-router-dom";
import { getInvoices } from "../data";
export default function Invoices() {
let invoices = getInvoices();
let [searchParams, setSearchParams] = useSearchParams();
return (
<div style={{ display: "flex" }}>
<nav
style={{
borderRight: "solid 1px",
padding: "1rem"
}}
>
<input
value={searchParams.get("filter") || ""}
onChange={event => {
let filter = event.target.value;
if (filter) {
setSearchParams({ filter });
} else {
setSearchParams({});
}
}}
/>
{invoices
.filter(invoice => {
let filter = searchParams.get("filter");
if (!filter) return true;
let name = invoice.name.toLowerCase();
return name.startsWith(filter.toLowerCase());
})
.map(invoice => (
<NavLink
style={({ isActive }) => ({
display: "block",
margin: "1rem 0",
color: isActive ? "red" : ""
})}
to={`/invoices/${invoice.number}`}
key={invoice.number}
>
{invoice.name}
</NavLink>
))}
</nav>
<Outlet />
</div>
);
}
setSearchParams()
is putting the?filter=...
search params in the URL and rerendering the router.useSearchParams
is now returning aURLSearchParams
withfilter
as one of its values.
persist the query string when we click a link by adding it to the link's href
import { useLocation, NavLink } from "react-router-dom";
function QueryNavLink({ to, ...props }) {
let location = useLocation();
return <NavLink to={to + location.search} {...props} />;
}
useLocation returns a location that tells us information about the URL. A location looks something like this:
{
pathname: "/invoices",
search: "?filter=sa",
hash: "",
state: null,
key: "ae4cz2j"
}
replace your NavLink
in src/routes/invoices.jsx
with QueryNavLink
<Link to="/shoes?brand=nike">Nike</Link>
<Link to="/shoes?brand=vans">Vans</Link>
then you wanted to style them as active
when the url search params match the brand
function BrandLink({ brand, ...props }) {
let [params] = useSearchParams();
let isActive = params.getAll("brand").includes(brand);
return (
<Link
style={{ color: isActive ? "red" : "" }}
to={`/shoes?brand=${brand}`}
{...props}
/>
);
}
be active when there's only one brand selected:
let brands = params.getAll("brand");
let isActive = brands.includes(brand) && brands.length === 1;
clicking Nike and then Vans adds both brands to the search params
function BrandLink({ brand, ...props }) {
let [params] = useSearchParams();
let isActive = params.getAll("brand").includes(brand);
if (!isActive) {
params.append("brand", brand);
}
return (
<Link
style={{ color: isActive ? "red" : "" }}
to={`/shoes?${params.toString()}`}
{...props}
/>
);
}
Or maybe you want it to add the brand if it's not there already and remove it if it's clicked again!
function BrandLink({ brand, ...props }) {
let [params] = useSearchParams();
let isActive = params.getAll("brand").includes(brand);
if (!isActive) {
params.append("brand", brand);
} else {
params = new URLSearchParams(
Array.from(params).filter(
([key, value]) => key !== "brand" || value !== brand
)
);
}
return (
<Link
style={{ color: isActive ? "red" : "" }}
to={`/shoes?${params.toString()}`}
{...props}
/>
);
}
import { useParams, useNavigate } from "react-router-dom";
import { getInvoice, deleteInvoice } from "../data";
export default function Invoice() {
let navigate = useNavigate();
let params = useParams();
let invoice = getInvoice(parseInt(params.invoiceId, 10));
return (
<main style={{ padding: "1rem" }}>
<h2>Total Due: {invoice.amount}</h2>
<p>
{invoice.name}: {invoice.number}
</p>
<p>Due Date: {invoice.due}</p>
<p>
<button
onClick={() => {
deleteInvoice(invoice.number);
navigate("/invoices");
}}
>
Delete
</button>
</p>
</main>
);
}