17. React Router, CSS Modules, Context API, State Management

npm install react-router-dom@6 #for this project we will be using router v6

import { BrowserRouter, Route, Routes } from "react-router-dom";
import Product from "./pages/Homepage";
import Homepage from "./pages/Homepage";
import Pricing from "./pages/Pricing";
import PageNotFound from "./pages/PageNotFound";

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Homepage />} />
        <Route path="product" element={<Product />} />
        <Route path="pricing" element={<Pricing />} />
        <Route path="*" element={<PageNotFound />} /> {/* This route will be matched if no other routes are matched */}
      </Routes>
    </BrowserRouter>
  );
}

export default App;
<a href="/pricing">Pricing</a> {/* Avoid using this as it reolads the entire page */}
import { Link } from "react-router-dom"

<Link to="/pricing">Pricing</Link> // use this instead

Difference between Link and NavLink :

  • With NavLink, react adds the class name "active" to the NavLink component which is currently active/opened.
    Pasted image 20240615001957.png
    This allows us to add CSS to the active NavLinks.

CSS Styling Options

Pasted image 20240615002913.png

CSS Modules :

It comes pre installed with both CRA and Vite.
What we do with CSS Modules is create 1 CSS file per component.
Pasted image 20240615005401.png
We can't use element selectors (e.g. ul) in CSS Modules. If we do so then it will select all the elements (here all the ul elements) in the entire application.
However we can do something like this instead :

  • Pasted image 20240615010002.png
    This completely valid and selects all the ul elements inside component we decide to apply this css on.

To apply the CSS from CSS Modules files :

Pasted image 20240615010232.png

  • We can consider .nav as an Object for easier understanding on how the CSS would be applied.

We can also directly destructure the object when importing the css file like this

  • Pasted image 20240615012008.png
    However this method is not recommended as it reduces code readability.

We can still import Global CSS files and they'd work usual.

  • Pasted image 20240615012825.png

Applying CSS from CSS Modules Globally :

Since a class name in CSS Modules is postfixed with some random ID therefore we can't just do this

<div className="nav" > Hello </div>

This will simply just not work.

To make it work though, we'll have to do this (inside the SomeCssFile.module.css file of course):
Pasted image 20240615013918.png
Now we can simply use .test class to add this CSS to that component.

Another example of applying CSS to .active class of NavLinks :

  • Pasted image 20240615014429.png
    Here simply doing .nav .active {...} would not have worked, as the .active class would have gotten some random ID postfixed to it.

Adding multiple style objects :

Pasted image 20240620135410.png

  • If we want to apply both of these styles then we'd do it like this :
import styles from './Button.module.css'

<button onClick={onClick} className={`${styles.btn} ${styles.primary}`} >
	{children}
</button>

or like this if we want to make the button reusable : (notice the type variable and how it is being used)
Pasted image 20240620135621.png

Nested Routes:

Image below shows an example of nested routes.
Pasted image 20240616014140.png

  • Notice how in path description we don't have to include the parent path. React is smart enough to combine these two.

Now, the place where the components of the child routes will be rendered is dependent on the React's Outlet component.

  • Pasted image 20240616014339.png
  • Pasted image 20240616014705.png

However notice that when we are at the /app url no child routes are rendered
Pasted image 20240618124800.png
This is not something ghat we want.
We can fix it using Index Route.

Index Route

  • It is basically the default child route which will be matched if none of the other routes matches.
    Pasted image 20240618125514.png

Storing state in the URL :

Pasted image 20240619132204.png
Pasted image 20240619132339.png
Pasted image 20240619132518.png

Example :

(see 11-worldwise) for full project and implementation details

Pasted image 20240619140708.png
Pasted image 20240619140725.png
Pasted image 20240619141021.png

Pasted image 20240619140901.png

  • id is the key name because we had defined it in the route <Route path="cities/:id" element={<City />} />

useSearchParams()

Mapjsx

import React from "react";
import styles from "./Map.module.css";
import { useSearchParams } from "react-router-dom";

export default function Map() {
  const [searchParams, setSearchParams] = useSearchParams(); // here searchParams is used to read the search parameters from the url and setSearchParams is used to set / update the searchParams

  // URL Right now : http://localhost:5173/app/cities/73930385?lat=38.727881642324164&lng=-9.140900099907554

  //  let us now store the search parameters in a variable since searchParams is not a normal object and we cannot use it to store the values, instead it's an object on which we have to use "get" methods to get the values

  const lat = searchParams.get("lat");
  const lng = searchParams.get("lng");

  return (
    <div className={styles.mapContainer}>
      <h1>Map</h1>
      <h1>
        Position: {lat}, {lng}
      </h1>
      <button onClick={() => {
        setSearchParams({ lat: 99, lng: 99}) // here we have to pass a brand new object with the updated values of the search parameters with all the values that we want to keep the same and the new values that we want to update, if we do not pass the values that we want to keep the same then they will be removed from the URL
      }}>Change pos</button>
    </div>
  );
}
  • Pasted image 20240620125742.png
    Before Clicking the button

  • Pasted image 20240620125843.png
    After clicking the button. Note that the state has been changed and the new / updated state is reflected everywhere.

Programmatic Navigation :

Using : useNavigate() :

  • Programmatic Navigation : It basically means to move to a new URL without the user having to click on any link
  • A common use case of this behaviour is right after submitting a form. So, many times when a user submits we want them to move to a new page automatically without them clicking anything.
    • For e.g. : On clicking somewhere inside the map container we then want to move automatically to the form component.
  • We can also navigate back using this hook.
  • -x (x=integer) in the navigate function represents the number of steps we want to go back. A positive value will represent the number of steps we want to go forward to.

e.g. (only for representation of the hook, a lot of the unrelated code is deleted to make it look less cluttery)

import { useState } from "react";

import styles from "./Form.module.css";
import Button from "./Button";
import { useNavigate } from "react-router-dom";

function Form() {

  const navigate = useNavigate();

  return (
    <form className={styles.form}>
      <div className={styles.buttons}>
        <Button type={"primary"}>Add</Button>
        <Button
          type={"back"}
          onClick={(e) => {
            e.preventDefault(); // we do this because the button is inside a form and we do not want the form to submit when we click the back button
            navigate(-1); // '-x' means go back 'x' step in the history, we can also pass a string to navigate to a specific route
          }}
        >
          &larr; Back
        </Button>
      </div>
    </form>
  );
}
export default Form;

To use replace option do this:

navigate("/app", { replace: true });

Using : <Navigate />

This is mostly used in nested routes.

e.g. :

  • Before :
    Pasted image 20240620143048.png
    This would by default Route us to the route : http://localhost:5173/app instead of http://localhost:5173/app/cities which is the URL we want to go to directly

  • After :
    Pasted image 20240620143340.png

Now, the problem is that we cannot go back because we got directed from /app to /app/cities and whenever we try to go back we'll be going back to /app which will again redirect us to the /app/cities route.

To fix this we'll add replace keyword like this :

  • <Route index element={<Navigate replace to="cities" />} /> : what this does it replace the current element in the history stack

Context API :

Pasted image 20240621115500.pngPasted image 20240621115753.png

createContext() :

see full code later in the document

useContext :

Pasted image 20240621124251.png

  • Output
    Pasted image 20240621124309.png

  • const { onClearPosts } = useContext(PostContext); : Direct destructuring.

PostProvider.js

import { faker } from "@faker-js/faker";
import { createContext, useContext, useState } from "react";

function createRandomPost() {
  return {
    title: `${faker.hacker.adjective()} ${faker.hacker.noun()}`,
    body: faker.hacker.phrase(),
  };
}

/* ----------------- 1. Create a context ---------------- */
const PostContext = createContext(); // notice how the variable name starts with a capital letter, this is because it's a component

function PostProvider({ children }) {
  const [posts, setPosts] = useState(() =>
    Array.from({ length: 30 }, () => createRandomPost())
  );
  const [searchQuery, setSearchQuery] = useState("");

  // Derived state. These are the posts that will actually be displayed
  const searchedPosts =
    searchQuery.length > 0
      ? posts.filter((post) =>
          `${post.title} ${post.body}`
            .toLowerCase()
            .includes(searchQuery.toLowerCase())
        )
      : posts;

  function handleAddPost(post) {
    setPosts((posts) => [post, ...posts]);
  }

  function handleClearPosts() {
    setPosts([]);
  }
  return (
    <PostContext.Provider
      value={{
        posts: searchedPosts,
        onAddPost: handleAddPost,
        onClearPosts: handleClearPosts,
        searchQuery,
        setSearchQuery,
      }}
    >
      {children}
    </PostContext.Provider>
  );
}

// Custom Hook :
function usePosts() {
  const context = useContext(PostContext);
  if(context === undefined) throw new Error('Post context was used outside of the PostProvider')
  return context;
}
// OR
const usePosts2 = () => useContext(PostContext);

export { PostProvider, PostContext, usePosts, usePosts2 };

App.js

import { createContext, useContext, useEffect, useState } from "react";
import { faker } from "@faker-js/faker";
// import { PostProvider, PostContext } from "./PostProvider";
import { PostProvider, usePosts } from "./PostProvider";

function createRandomPost() {
  return {
    title: `${faker.hacker.adjective()} ${faker.hacker.noun()}`,
    body: faker.hacker.phrase(),
  };
}

function App() {
  const [isFakeDark, setIsFakeDark] = useState(false);

  // Whenever `isFakeDark` changes, we toggle the `fake-dark-mode` class on the HTML element (see in "Elements" dev tool).
  useEffect(
    function () {
      document.documentElement.classList.toggle("fake-dark-mode");
    },
    [isFakeDark]
  );

  return (
    <PostProvider>
      <section>
        <button
          onClick={() => setIsFakeDark((isFakeDark) => !isFakeDark)}
          className="btn-fake-dark-mode"
        >
          {isFakeDark ? "☀️" : "🌙"}
        </button>

        <Header />
        <Main />
        <Archive />
        <Footer />
      </section>
    </PostProvider>
  );
}

function Header() {
  // 3) Consuming Context Value
  const { onClearPosts } = usePosts();
  return (
    <header>
      <h1>
        <span>⚛️</span>The Atomic Blog
      </h1>
      <div>
        <Results />
        <SearchPosts />
        <button onClick={onClearPosts}>Clear posts</button>
      </div>
    </header>
  );
}

function SearchPosts() {
  const { searchQuery, setSearchQuery } = usePosts();
  return (
    <input
      value={searchQuery}
      onChange={(e) => setSearchQuery(e.target.value)}
      placeholder="Search posts..."
    />
  );
}

function Results() {
  const { posts } = usePosts();
  return <p>🚀 {posts.length} atomic posts found</p>;
}

function Main() {
  return (
    <main>
      <FormAddPost />
      <Posts />
    </main>
  );
}

function Posts() {
  return (
    <section>
      <List />
    </section>
  );
}

function FormAddPost() {
  const { onAddPost } = usePosts();
  const [title, setTitle] = useState("");
  const [body, setBody] = useState("");

  const handleSubmit = function (e) {
    e.preventDefault();
    if (!body || !title) return;
    onAddPost({ title, body });
    setTitle("");
    setBody("");
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        placeholder="Post title"
      />
      <textarea
        value={body}
        onChange={(e) => setBody(e.target.value)}
        placeholder="Post body"
      />
      <button>Add post</button>
    </form>
  );
}

function List() {
  const { posts } = usePosts();

  return (
    <ul>
      {posts.map((post, i) => (
        <li key={i}>
          <h3>{post.title}</h3>
          <p>{post.body}</p>
        </li>
      ))}
    </ul>
  );
}

function Archive() {
  const { onAddPost } = usePosts();

  // Here we don't need the setter function. We're only using state to store these posts because the callback function passed into useState (which generates the posts) is only called once, on the initial render. So we use this trick as an optimization technique, because if we just used a regular variable, these posts would be re-created on every render. We could also move the posts outside the components, but I wanted to show you this trick 😉
  const [posts] = useState(() =>
    // 💥 WARNING: This might make your computer slow! Try a smaller `length` first
    Array.from({ length: 10000 }, () => createRandomPost())
  );

  const [showArchive, setShowArchive] = useState(false);

  return (
    <aside>
      <h2>Post archive</h2>
      <button onClick={() => setShowArchive((s) => !s)}>
        {showArchive ? "Hide archive posts" : "Show archive posts"}
      </button>

      {showArchive && (
        <ul>
          {posts.map((post, i) => (
            <li key={i}>
              <p>
                <strong>{post.title}:</strong> {post.body}
              </p>
              <button onClick={() => onAddPost(post)}>Add as new post</button>
            </li>
          ))}
        </ul>
      )}
    </aside>
  );
}

function Footer() {
  return <footer>&copy; by The Atomic Blog ✌️</footer>;
}

export default App;

State :

Pasted image 20240621134751.pngPasted image 20240621143935.png
Pasted image 20240621144102.png