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

16. Next.js Tutorial (3). Dynamic Routes. Async functions getStaticPaths and getStaticProps