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

Entradas populares de este blog

14. Next.js Tutorial (1)

15. Next.js Tutorial (2). Fetching data. Async functions getStaticProps, getServerSideProps, getStaticPaths

17. Next.js Tutorial (4). API Routes