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
Link
and NavLink
:NavLink
, react adds the class name "active"
to the NavLink
component which is currently active/opened.It comes pre installed with both CRA and Vite.
What we do with CSS Modules is create 1 CSS file per component.
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 :
ul
elements inside component we decide to apply this css on..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
We can still import Global CSS files and they'd work usual.
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):
Now we can simply use .test
class to add this CSS to that component.
Another example of applying CSS to .active
class of NavLinks :
.nav .active {...}
would not have worked, as the .active
class would have gotten some random ID postfixed to it.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)
Image below shows an example of nested routes.
Now, the place where the components of the child routes will be rendered is dependent on the React's Outlet component.
However notice that when we are at the /app
url no child routes are rendered
This is not something ghat we want.
We can fix it using Index Route.
(see 11-worldwise) for full project and implementation details
id
is the key name because we had defined it in the route <Route path="cities/:id" element={<City />} />
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>
);
}
Before Clicking the button
After clicking the button. Note that the state has been changed and the new / updated state is reflected everywhere.
useNavigate()
:-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
}}
>
← Back
</Button>
</div>
</form>
);
}
export default Form;
To use replace option do this:
navigate("/app", { replace: true });
<Navigate />
This is mostly used in nested routes.
e.g. :
Before :
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 :
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 stacksee full code later in the document
Output
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>© by The Atomic Blog ✌️</footer>;
}
export default App;