3. React Routing (react-router-dom)
1. Create a link to a route
In this component, 2 links are created but do not open any page as NO ROUTES are defined
import React from 'react' import './App.css' import { Link } from "react-router-dom" export default function App() { return ( <div> <Link to="/invoices">Invoices</Link> |{" "} <Link to="/expenses">Expenses</Link> </div> ); }
2. Create the routes in the index.tsx
import ReactDOM from 'react-dom/client'; import { BrowserRouter, Route, Routes } from "react-router-dom"; import './index.css'; import App from './App'; import Expenses from './routes/Expenses' import Invoices from './routes/Invoices' const root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement ); root.render( <BrowserRouter> <Routes> <Route path="/" element={<App />} /> <Route path="expenses" element={<Expenses />} /> <Route path="invoices" element={<Invoices />} /> </Routes> </BrowserRouter> );
But the components <Expenses> and <Invoices> must be created
In this case, the <Expenses> is
import React from 'react' export default function Expenses() { return ( <main style={{ padding: "1rem 0" }}> <h2>Expenses</h2> </main> ); }
And the <Invoices> is the same but replacing "Expenses" with "Invoices"
3. Nesting the content
The target is displaying the <Expenses> and <Invoices> components into the <App> component.
This is accomplished by :
1. Nesting (Invoices and Expenses) <Route> into the (App) <Route> in "index.tsx"
import ReactDOM from 'react-dom/client'; import { BrowserRouter, Route, Routes } from "react-router-dom"; import './index.css'; import App from './App'; import Expenses from './routes/Expenses' import Invoices from './routes/Invoices' const root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement ); root.render( <BrowserRouter> <Routes> {/* Nesting (Expendes and Invoices) routes into App */} <Route path="/" element={<App />} > <Route path="expenses" element={<Expenses />} /> <Route path="invoices" element={<Invoices />} /> </Route> </Routes> </BrowserRouter> );
2. Insert the <Outlet /> tag at the end of the <App> component "App.tsx"
import React from 'react' import './App.css' import { Link, Outlet } 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> );
4. "No match" route
These steps 3 are needed:
1. Adding data (data/invoices.ts)
export interface invoiceType { name: string number: number amount: string due: string } let invoices: invoiceType[] = [ { 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/1998", }, ]; export function getInvoices() { return invoices; } export function getInvoice(number: number) { return invoices.find ( (e) => e.number===number )
}
2. Add links in the <Invoices> component
import Rearct from 'react' import { Link } from "react-router-dom" import { getInvoices } from "../data/Invoices" export default function Invoices() { const 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> ); }
3. Add "no match" route to index.tsx
import ReactDOM from 'react-dom/client'; import { BrowserRouter, Route, Routes } from "react-router-dom"; import './index.css'; import App from './App'; import Expenses from './routes/Expenses' import Invoices from './routes/Invoices' const root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement ); root.render( <BrowserRouter> <Routes> {/* Nesting (Expendes and Invoices) routes into App */} <Route path="/" element={<App />} > <Route path="expenses" element={<Expenses />} /> <Route path="invoices" element={<Invoices />} /> <Route path="*" element={ <main style={{ padding: "1rem" }}> <p>There is nothing here!</p> </main> } /> </Route> </Routes> </BrowserRouter> );
5. Adding URL params (route/param)
Let's add some more code to the index.tsx file, nesting the param part
import ReactDOM from 'react-dom/client'; import { BrowserRouter, Route, Routes } from "react-router-dom"; import './index.css'; import App from './App'; import Expenses from './routes/Expenses' import Invoices from './routes/Invoices' import Invoice from './routes/Invoice'; const root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement ); root.render( <BrowserRouter> <Routes> {/* Nesting (Expendes and Invoices) routes into App */} <Route path="/" element={<App />} > <Route path="expenses" element={<Expenses />} /> <Route path="invoices" element={<Invoices />} > <Route path=":invoiceId" element={<Invoice />} /> <Route path="*" element={ <main style={{ padding: "1rem" }}> <p>There's nothing here!</p></main>
}
/>
/Route>
</Route>
</Routes> </BrowserRouter> );
Now let's read the param and get the invoice and display its data (invoice.tsx). Note that typescript complains when assigning a type string to type string | undefined, so it is used de ternary operator.
import React from 'react' import { useParams } from 'react-router-dom'; import { getInvoice, invoiceType } from '../data/Invoices'; export default function Invoice() { const params = useParams(); const invoiceId: string = params.invoiceId ? params.invoiceId : "0" const invoice: invoiceType | undefined = getInvoice(parseInt(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> </main> ); }
Note that "invoideId" is defined in index.tsx and invoice.tsx with the same name
<Route path=":invoiceId" element={<Invoice />} /> // index.tsx
const invoiceId: string = params.invoiceId ? params.invoiceId : "0" // invoice.tsx
6. Adding index routes to index.tsx
In this case, we are filling the empty space when no invoice is selected
import ReactDOM from 'react-dom/client';
import { BrowserRouter, Route, Routes } from "react-router-dom";
import './index.css';
import App from './App';
import Expenses from './routes/Expenses'
import Invoices from './routes/Invoices'
import Invoice from './routes/Invoice';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<BrowserRouter>
<Routes>
{/* Nesting (Expendes and Invoices) routes into App */}
<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 path="*"
element={
<main style={{ padding: "1rem" }}>
<p>There is nothing here!</p>
</main>
}
/>
</Route>
</Route>
</Routes>
</BrowserRouter>
);
7. Active links (Navlinks)
In this case, let's change <Link> with <NavLink> and set state style color to state. (The color changes to red when the link is clicked)
import Rearct from 'react' import { Link, NavLink, Outlet } from "react-router-dom" import { getInvoices } from "../data/Invoices" export default function Invoices() { const invoices= getInvoices() return ( <div style={{ display: "flex" }}> <nav style={{ borderRight: "solid 1px", padding: "1rem", }} > {invoices.map((invoice) => ( <NavLink className={({ isActive }) => isActive ? "red" : "blue"} style={{ display: "block", margin: "1rem 0" }} to={`/invoices/${invoice.number}`} key={invoice.number} > {invoice.name} </NavLink> ))} </nav> <Outlet /> </div> ); }
8. Search params (/route?name=value)
Search params are the same as URL params (placed after the slash (/) ) but they are placed after a question mark (?) . In this case, Now the Invoices.tsx is as follows:
Here is the Invoices.tsx file
import Rearct from 'react' import { Link, NavLink, Outlet, useSearchParams} from "react-router-dom" import { getInvoices } from "../data/Invoices" export default function Invoices() { const invoices= getInvoices() const [searchParams, setSearchParams] = useSearchParams() return ( <div style={{ display: "flex" }}> <nav style={{ borderRight: "solid 1px", padding: "1rem", }} > <input value={searchParams.get("filter") || ""} onChange={(event) => { const filter= event.target.value if (filter) { setSearchParams({filter}) }else { setSearchParams({}) } }} /> {invoices.filter((invoice) => { const filter=searchParams.get("filter") if (!filter) return true const name = invoice.name.toLowerCase(); return name.startsWith(filter.toLowerCase()) }) .map((invoice) => ( <NavLink className={({ isActive }) => isActive ? "red" : "blue"} style={{ display: "block", margin: "1rem 0" }} to={`/invoices/${invoice.number}`} key={invoice.number} > {invoice.name} </NavLink> ))} </nav> <Outlet /> </div> ); }
9. Custom behavior
When clicking an invoice, the search filter is cleaned!
Combining NavLink and useLocation hook into a new component may help, but this is a bit tricky as the code from the documentation has compilation errors with typescript. The solution is from minicodelab.dev and some explanation from Borislav Hadzhiev:
import React from "react"; import { useLocation, NavLink, NavLinkProps } from "react-router-dom"; // ERROR:1 No type definition in arguments // ERROR:2 Missing component children as React.ReactNode // NOTE: You can use {to:string; children:React.ReactNode} & NavLinkProps //export default function QueryNavLink({ to, ...props }) { export default function QueryNavLink( { to, children, ...props }: {to:string; children:React.ReactNode} & NavLinkProps ) { const location = useLocation(); return ( <> {/* No Children added !! <NavLink to={to + location.search} {...props} />; */} <NavLink to={`${to}${location.search}`} {...props}> {children} </NavLink> </> ) }
As the "location" object has this structure, we can get any param related to the route
{
pathname: "/invoices",
search: "?filter=sa",
hash: "",
state: null,
key: "ae4cz2j"
}
10. Navigating programmatically
To navigate to an URL the "useNavigate" hook gets nice. Also "useParams" and "useLocation" can help
Here is an example where a delete button is added and redirects to the invoice menu, conserving the filter
import React from 'react' import { useLocation, useNavigate, useParams } from 'react-router-dom'; import { getInvoice, invoiceType, deleteInvoice } from '../data/Invoices'; export default function Invoice() { const params = useParams(); const navigate= useNavigate() const location = useLocation() const invoiceId: string = params.invoiceId ? params.invoiceId : "0" let invoice: invoiceType | undefined = getInvoice(parseInt(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={() => { if (invoice) deleteInvoice(invoice.number) navigate('/invoices' + location.search) }} > Delete </button> </p> </main> ); }
If the filter is "sa" the url generated is "/invoices?filter=sa" as location.search stores the value "filter=sa"
Comentarios
Publicar un comentario