👋👋 You can switch between 4 branches I made during making this project to make this project less overwhelming 👋👋
Topics covered in coresponding branch part
Route params
,Routes
,Link
,Outlet
Nested Routes
,Relative and Absolute Paths
,NavLinks
,Outlet Context
Search Params
,Filtering with Params
,Link state
,Error Handling
History stack
,Layer API
,Loaders
,Actions
,Forms
,defer
,Await
for accessing Host path use login: b@b.com password: p123
If we want to display certain components on multiple pages but extend content.
Very similar approach like react {children}
.
index.jsx > App()
<BrowserRouter>
<Routes>
<Route element={<Layout />}>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/vans" element={<VansList />} />
<Route path="/vans" element={<VansList />} />
<Route path="vans/:id" element={<VanDetail />} />
</Route>
</Routes>
</BrowserRouter>
Layout.jsx
const Layout = () => {
return (
<>
<Header />
<Outlet /> // functions like react children
</>
);
};
In this example every component inside <Route element={<Layout />}>
route will display Header
component and will be extended by content of child route.
So for example on path "/"
app will display Home
component and Header
component above.
Often parent routes manage state or other values you want shared with child routes. You can create your own context provider if you like, but this is such a common situation that it's built-into <Outlet />
:
<Route path="vans/:id" element={<HostVanDetail />}>
<Route index element={<HostVanInfo />} />
<Route path="pricing" element={<HostVanPricing />} />
<Route path="photos" element={<HostVanPhotos />} />
</Route>
parent (HostVanDetail)
... fetching code etc
return (
<Outlet context={{ currentVan }} />
)
child (HostVanPhotos)
import { useOutletContext } from "react-router-dom";
export default function HostVanPhotos() {
const { currentVan } = useOutletContext();
return <img src={currentVan.imageUrl} className="host-van-detail-image" />;
}
<Route path="host" element={<HostLayout />}>
<Route index element={<Dashboard />} />
<Route path="income" element={<Income />} />
<Route path="reviews" element={<Reviews />} />
</Route>
We are omitting the /home because due to fact that Dashboard
Income
and Reviews
component Route
s are inside HostLayout
they somewhat extend the parents path which is host.
It can lead to less repetitive code if we have a lot of nested routes.
<Route path="/host" element={<HostLayout />}>
<Route path="/host" element={<Dashboard />} />
<Route path="/host/income" element={<Income />} />
<Route path="/host/reviews" element={<Reviews />} />
</Route>
Due the fact that we cant use path="/" in Relative approach, because it would mean that we are referring to absolute path, we can point our, in this example Dashboard
component's path as index
.
I understand it as a somewhat alternative to path='' but cleaner and nicer solution :)
<Route path="host" element={<HostLayout />}>
// index component
<Route index element={<Dashboard />} />
<Route path="income" element={<Income />} />
<Route path="reviews" element={<Reviews />} />
</Route>
It will render
Dashboard
component on path '/host' in our browser.
NavLink allows us to access the isActive
property that's make it much easier to implement functionality for highlighting or just changing CSS of currently displayed component and reflecting it in our Nav.
Header.jsx
<NavLink
className={({ isActive }) => (isActive ? "link--is-active" : null)}
to="/vans"
>
Vans
</NavLink>
index.css
.link--is-active {
font-weight: bold;
font-size: 105%;
color: #303030;
}
The end
prop changes the matching logic for the active
and pending
states to only match to the "end" of the NavLinks's to
path. If the URL is longer than to
, it will no longer be considered active.
Without the end prop, this link is always active because every URL matches /
.
<NavLink to="/">Home</NavLink>
To match the URL "to the end" of to
, use end
:
<NavLink to="/" end>
Home
</NavLink>
Now this link will only be active at "/"
.
Alternatively to relative and absolute paths, we can also use Relative Links.
index.jsx
<Route path="host" element={<HostLayout />}>
<Route index element={<Dashboard />} />
<Route path="income" element={<Income />} />
<Route path="reviews" element={<Reviews />} />
<Route path="vans" element={<HostVans />} />
<Route path="vans/:id" element={<HostVanDetail />} />
</Route>
If we want to use Link
in one of our children components we dont have to use absolute path like this
<Link to={`host/vans/${id}`}> Go to details </Link>
Instead of this, we can make usage of relativity to the parents path.
So If we are rendering a Link
inside parent Route which path is for example "/host", we can just write to
property like this
<Link to={`vans/${id}`}> Go to details </Link>
Which will result in the same as code above.
'.'
value inside to
property will mean that we want to go to our parent's route path. In this exact example it will navigate us to the /host
path
You can think of this like moving through directories in terminal where
-
cd
.
ismove to the current directory
-
cd
..
ismove to the parent directory
HostLayout.jsx (Navbar)
<nav className="host-nav">
<NavLink
to="." // /host
end
style={({ isActive }) => isActive ? activeStyles : null}
>
Dashboard
</NavLink>
<NavLink
to="income" // /host/income
style={({ isActive }) => isActive ? activeStyles : null}
>
Income
</NavLink>
<NavLink
to="vans" // /host/vans
style={({ isActive }) => isActive ? activeStyles : null}
>
Vans
</NavLink>
<NavLink
to="reviews" //host/reviews
style={({ isActive }) => isActive ? activeStyles : null}
>
Reviews
</NavLink>
</nav>
<Outlet />
</>
<Route path="host" element={<HostLayout />}>
<Route index element={<Dashboard />} />
<Route path="income" element={<Income />} />
<Route path="reviews" element={<Reviews />} />
<Route path="vans" element={<HostVans />} />
<Route path="vans/:id" element={<HostVanDetail />} />
</Route>
In scenario where we are rendering HostVanDetails
which is under
path**'/host/vans/13'
**, If we would use Link
with to=".."
we wouldn't be
navigated to host/vans
as we could think.
Instead of we would be navigated to /host
because this is the parent path
to achieve situation where clicking a Link
which navigates us back to the list of vans component which is displayed under /host/vans
path, we will have to specify relative
property inside Link
to "path"
like this:
<Link to=".." relative="path">
Back to the list
</Link>
-
Represent a change in the UI
- Sorting, filtering, pagination
-
Used as a "single source of truth" for certain application state
- Ask yourself "Should a user be able to revisit or share this page just like it is?" If yes, then you might consider raising that state up to the URL in a query parameter
-
Key/value paris in the URL
-
Begins with
?
vans?type=rugged
- type is obj key and rugged is obj value
-
Separated by
&
- vans?type=rugged&filterBy=price
The useSearchParams
hook is used to read and modify the query string in the URL for the current location. Like React's own useState
hook, useSearchParams
returns an array of two values: the current location's search params and a function that may be used to update them.
url: /vans?type=blahblahblah
const [searchParams, setSearchParams] = useSearchParams();
const typeFilter = searchParams.get("type");
console.log(typeFilter);
// blahblahblah
...
const typeFilter = searchParams.get("type")
const displayedVans = typeFilter
? vans.filter(van => van.type === typeFilter)
: vans
const vanElements = displayedVans.map(van => ... )
return (
<>
<div className="van-list-filter-buttons">
<Link className="van-type simple" to="?type=simple"> simple </Link>
<Link className="van-type luxury" to="?type=luxury"> luxury </Link>
<Link className="van-type rugged" to="?type=rugged"> rugged </Link>
<Link className="van-type clear-filters" to="."> clear </Link>
</div>
<div className="van-list">
{vanElements}
</div>
</>
)
Using setSearchParams allows us for more complex and flexible params.
<div className="van-list-filter-buttons">
<button onClick={() => setSearchParams({ type: "simple" })}> simple </button>
<button onClick={() => setSearchParams({ type: "luxury" })}> luxury </button>
<button onClick={() => setSearchParams({ type: "rugged" })}> rugged </button>
<button onClick={() => setSearchParams({})}> clear </button>
</div>
Using Links is only for very simple scenarios and is rather discouraged
The state
property can be used to set a stateful value for the new location which is stored inside history state. This value can subsequently be accessed via useLocation()
.
<Link to={van.id} state={{ search: searchParams.toString() }}>
import {useLocation} from "react-router-dom"
...
const location = useLocation();
console.log(location)
// {pathname: "/vans/5", search: "", hash: "",
// state: {search: "type=luxury"}, key: "emy8w7js"}
const search = location.state?.search || "";
return (
<Link to={`..${search}`} path="relative" className="back-button">
Back to all vans
</Link>
);
- Happy Path
- Assumes everything goes according to plan exactly as we hope it does
- Doesn't account for errors or other problems that could occur
- Sad Path
- Forces us to imagine what could go wrong and plan accordingly
- Error handling, loading states, form validation, empty state etc.
Basically catch-all
route is using *
as a path.
In React-Router-V6 we don't have to worry about index of error page anymore.
Due the fact that ErrorPage
Route is a children of a Layout
component, we still will be able to see navbar and footer instead of blank page.
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="about" element={<About />} />
<Route path="vans" element={<Vans />} />
<Route path="vans/:id" element={<VanDetail />} />
<Route path="host" element={<HostLayout />}>
<Route index element={<Dashboard />} />
<Route path="income" element={<Income />} />
<Route path="reviews" element={<Reviews />} />
<Route path="vans" element={<HostVans />} />
<Route path="vans/:id" element={<HostVanDetail />}>
<Route index element={<HostVanInfo />} />
<Route path="pricing" element={<HostVanPricing />} />
<Route path="photos" element={<HostVanPhotos />} />
</Route>
</Route>
<Route path="*" element={<ErrorPage />} /> // HERE IS CATCH-ALL ROUTE
</Route>
</Routes>
</BrowserRouter>
React-Router under the hood is transforming Route components into plain JS objects
so
<Routes>
<Route path="/" element={<HomePage />} />
</Routes>
becomes an array of objects
[
{
path: "/",
element: <HomePage />,
children: [{}],
},
];
If we want to make use of Data Layers
api we have to transform our Route components into browserRouter. Fortunately there is a function createRoutesFromElements
which does exactly that what you think :)
import {
createRoutesFromElements,
createBrowserRouter,
} from "react-router-dom";
const router = createBrowserRouter(
createRoutesFromElements(<Route path="/" element={<HomePage />} />)
);
So basically we have 2 options now when we are creating the Route we can either
- go old fashioned way with element Route and then use createRoutesFromElements
- go straight into writing plain JavaScript objects manually.
Example of transferring element routes and implementing browserRouter
Index.jsx without browserRouter
import ...
import {
BrowserRouter,
Route,
Link,
RouterProvider,
createBrowserRouter,
createRoutesFromElements } from "react-router-dom"
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="about" element={<About />} />
<Route path="vans" element={<Vans />} />
<Route path="vans/:id" element={<VanDetail />} />
<Route path="host" element={<HostLayout />}>
<Route index element={<Dashboard />} />
<Route path="income" element={<Income />} />
<Route path="reviews" element={<Reviews />} />
<Route path="vans" element={<HostVans />} />
<Route path="vans/:id" element={<HostVanDetail />}>
<Route index element={<HostVanInfo />} />
<Route path="pricing" element={<HostVanPricing />} />
<Route path="photos" element={<HostVanPhotos />} />
</Route>
</Route>
<Route path="*" element={<NotFound />}/>
</Route>
</Routes>
</BrowserRouter>
)
}
ReactDOM
.createRoot(document.getElementById('root'))
.render(<App />);
Index.jsx with browserRouter
import ...
import {
BrowserRouter,
Route,
Link,
RouterProvider,
createBrowserRouter,
createRoutesFromElements } from "react-router-dom"
const router = createBrowserRouter(createRoutesFromElements(
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="about" element={<About />} />
<Route path="vans" element={<Vans />} />
<Route path="vans/:id" element={<VanDetail />} />
<Route path="host" element={<HostLayout />}>
<Route index element={<Dashboard />} />
<Route path="income" element={<Income />} />
<Route path="reviews" element={<Reviews />} />
<Route path="vans" element={<HostVans />} />
<Route path="vans/:id" element={<HostVanDetail />}>
<Route index element={<HostVanInfo />} />
<Route path="pricing" element={<HostVanPricing />} />
<Route path="photos" element={<HostVanPhotos />} />
</Route>
</Route>
<Route path="*" element={<NotFound />}/>
</Route>
))
function App() {
return (
<RouterProvider router={router} />
)
}
ReactDOM
.createRoot(document.getElementById('root'))
.render(<App />);
TL:DR benefit: inside a component where you would do fetching in classic react way, you can behave like data is always already there.
- Export a loader function from the page that fetches the data that page will need.
- Pass a loader prop to the Route that renders that page and pass in the loader function
- Use the useLoaderData hook in the component to get the data
api.js
export async function getVans() {
const res = await fetch("/api/vans");
if (!res.ok) {
throw {
message: "Failed to fetch vans",
statusText: res.statusText,
status: res.status,
};
}
const data = await res.json();
return data.vans;
}
index.jsx
import Vans, { loader as vansLoader } from "./pages/Vans/Vans"
//inside of createRoutesFromElements
...
<Route path="vans" element={<Vans />} loader={vansLoader} />
...
Vans.jsx
import { Link, useSearchParams, useLoaderData } from "react-router-dom";
import { getVans } from "../../api";
export function loader() {
return getVans();
}
// Rest of the code
OKAY BUT WHAT ALL OF THIS DOES?
now fetching the data doesn't take place like it always used to be in useEffect
loader function is being called when user wants to navigate to path which Route
has loader
parameter. So for example when clicking a NavLink to /about
now when user which is for example on the /home
route wants to navigate to the /vans
via clicking on the correct NavLink
, fetching will start to take place and Vans component WILL RENDER AFTER FETCHING WILL COMPLETE AND EVERYTHING WILL BE READY TO ACTUALLY RENDER THE COMPONENT
okay but what with UX? check this Deferring data
so basically: no error handling, no loading state, no useEffect and all of that inside actual component which is HUGE.
Among new features that can be now passed inside Route (Object or Element :) )
we can pass errorElement
if actual element
throws any error.
index.jsx
<Route
path="vans"
element={<Vans />}
loader={vansLoader}
errorElement={<ErrorDisplayer />} //errorElement which catches errors
/>
api.js
export async function getVans() {
const res = await fetch("/api/vans");
if (!res.ok) {
throw {
message: "Failed to fetch vans",
statusText: res.statusText,
status: res.status,
};
}
const data = await res.json();
return data.vans;
}
ErrorDisplayer.jsx
import { useRouteError } from "react-router-dom";
export default function Error() {
const error = useRouteError();
console.log(error);
return (
<>
<h1> Error: {error.message} </h1>
<pre>
{" "}
{error.status} - {error.statusText}{" "}
</pre>
</>
);
}
// {message: "Failed to fetch vans", statusText: "Bad Request", status: 400}
<Route path="/" element={<Layout />} errorElement={<Error />}>
{" "}
// ERROR ELEMENT HERE
<Route index element={<Home />} />
<Route path="about" element={<About />} />
<Route path="vans" element={<Vans />} loader={vansLoader} />
<Route path="vans/:id" element={<VanDetail />} />
<Route path="host" element={<HostLayout />}>
<Route index element={<Dashboard />} />
<Route path="income" element={<Income />} />
<Route path="reviews" element={<Reviews />} />
<Route path="vans" element={<HostVans />} />
<Route path="vans/:id" element={<HostVanDetail />}>
<Route index element={<HostVanInfo />} />
<Route path="pricing" element={<HostVanPricing />} />
<Route path="photos" element={<HostVanPhotos />} />
</Route>
</Route>
<Route path="*" element={<NotFound />} />
</Route>
if we will setup our errorElement like this, it will catch every error that happens inside our App. (because errorElement is inside Layout which basically wraps our whole app).
on the other hand if we will set our errorElement down in some nested Route
it wouldn't catch errors that happens in ancestor routes.
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="about" element={<About />} />
<Route path="vans" element={<Vans />} loader={vansLoader} />
<Route path="vans/:id" element={<VanDetail />} />
<Route path="host" element={<HostLayout />}>
<Route index element={<Dashboard />} />
<Route path="income" element={<Income />} />
<Route path="reviews" element={<Reviews />} />
<Route path="vans" element={<HostVans />} errorElement={<Error />} /> // ERROR
ELEMENT HERE
<Route path="vans/:id" element={<HostVanDetail />}>
<Route index element={<HostVanInfo />} />
<Route path="pricing" element={<HostVanPricing />} />
<Route path="photos" element={<HostVanPhotos />} />
</Route>
</Route>
<Route path="*" element={<NotFound />} />
</Route>
In situation like this errorElement wont catch error which occurred inside
Vans
component
very simplified example of implementation of protected routes
AuthRequired.jsx
import { Outlet, Navigate } from "react-router-dom";
const AuthRequired = () => {
const token = { accessToken: "123" };
return (
<>
{token.accessToken ? <Outlet /> : <Navigate to="/login" />} // Navigate
will force redirect to /login page
</>
);
};
export default AuthRequired;
index.jsx
<Route element={<AuthRequired /> } /> // AuthRequired function will get called every time one of children routes will get called
<Route path="host" element={<HostLayout />}>
<Route index element={<Dashboard />} />
<Route path="income" element={<Income />} />
<Route path="reviews" element={<Reviews />} />
<Route path="vans" element={<HostVans />} />
<Route path="vans/:id" element={<HostVanDetail />}>
<Route index element={<HostVanInfo />} />
<Route path="pricing" element={<HostVanPricing />} />
<Route path="photos" element={<HostVanPhotos />} />
</Route>
</Route>
Inside Navigate
element we can pass state object just like in Link element.
we can use it to eventually restore the path from which user was redirected to login page or display the message why user was redirected to login page.
<Navigate to="/login" state={{ message: "You must log in first." }} />
Login.jsx
...
import { useLocation } from "react-router-dom"
const location = useLocation()
console.log(location)
// IF REDIRECTED FROM NAVIGATE - {pathname: "/login", search: "", hash: "", state: {message: "You must log in first."}, key: "z9k5z5r5"} - state contains message entry
// IF user navigated to login page by himself - {pathname: "/login", search: "", hash: "", state: null, key: "default"} - state is null
If user is being redirected to the login page after clicking protected route without authorization History stack will look like this in that exact situation
(after logging in user is being navigated to /host path again)
with replace
property inside Navigate
element or passed as parameter inside navigate
we can modify history stack
so for example if we will use
Login.jsx
const from = location?.state?.from || "/host";
navigate(from, { replace: true });
it will replace the /login
inside our history stack with /host
It has meaning if it comes to UX which if will hit back arrow inside browser wont be navigated back to the login page but to the previously visited path before redirect happened
Basically you can think about it that
replace
will replace current history stack element with following history stack element where you will be redirected or navigated
The Form component is a wrapper around a plain HTML form that emulates the browser for client side routing and data mutations.
<Form method="post" action="/events">
<input type="text" name="title" />
<input type="text" name="description" />
<button type="submit">Create</button>
</Form>
Make sure your inputs have names or else the
FormData
will not include that field's value.
All of this will trigger state updates to any rendered useNavigation
hooks so you can build pending indicators and optimistic UI while the async operations are in-flight.
the url to which the form will be submitted, just like HTML form action. Where the native element would submit to a backend, react-router-v6 Form
will run an action
function instead.
Login.jsx
export const action = async ({ request }) => {
console.log("form submitted, logged from action function", { request });
};
// form submitted, logged from action function, Request {url: "https://cw3.scrimba.com/login", credentials: "same-origin", headers: {map: {content-type: "application/x-www-form-urlenc..."}}, method: "POST", mode: null, signal: {}, referrer: null, bodyUsed: false, _bodyInit: {}, _bodyText: "email=blazej%40blazej.pl&pass..."}
export default function Login() {
const navigate = useNavigate();
return (
<Form action="/login" method="post">
{" "}
// WE ARE POINTING AT WHICH RELATIVE (!!!) PATH OUR ACTION FUNCTION IS BEING
LOCATED and method of request
<input type="email" name="email" placeholder="Email address" />
<br />
<input type="password" name="password" placeholder="Password" />
<br />
<button>Log in</button>
</Form>
);
}
In other words it means that
action
function for that Form can be found inside component that is rendered under/login
path.
index.jsx
import Login, {action as loginAction } from "./Login"
... router code
createRoutesFromElements(
<Route path="/" element={<Layout />}
<Route path="login" element={<Login />} action={loginAction} /> // we are providing an function that will be called when form which point to path /login will be submitted
</Route>
)
Login.jsx
export const action = async ({ request }) => {
const formData = await request.formData(); // creating formData object that is providing methods allowing us to menage and get data from form.
const email = formData.get("email") // we are passing input's name parameter from which we want to get submitted data.
const password = formData.get("password")
//process this info however we want
console.log(email, password)
const data = await fakeLoginUser({email, password})
return data
}
// bl@zej.com,"qazxsw" (logged after submitting the Form)
export default const Login = () => {
...
}
useActionData
hook provides the returned value from the previous navigation's action
result, or undefined
if there was no submission.
Login.jsx
import { useActionData } from "react-router-dom"
export const action = async ({ request }) => {
...
return data
}
export default const Login = () => {
const data = useActionData()
console.log(data)
...rest of Login code
}
// {token: "Here's your token!"} log action's function returned value which in that example is coming from fakeLoginUser function.
If we throw an error inside action function with Throw
, react router will try to find errorElement among the routes like I described above in Handling an error.
In our scenario we don't really want that to happen because that would unmount our Login.jsx component and wont provide valuable information to the user.
Instead of this, a better approach would be displaying short info above the form like "couldn't log you in" or something like this.
To achieve that we cant throw an error inside action function but we can use try{...} catch(e) {...} block and segment the code between happy-path and sad-path, sad patch will be placed inside catch block.
Login.jsx
export async function action({ request }) {
const formData = await request.formData()
const email = formData.get("email")
const password = formData.get("password")
try {
const data = await fakeLoginUser({ email, password })
return data
} catch (err) {
return {
error: err.message
}
}
}
export default const Login = () => {
const data = useActionData();
...
return (
{data?.error && <h4>{data.error}</h4>} //conditionally render error message
...rest of JSX
)
}
Login.jsx without Data Layer Api 🤢🤢
import React from "react";
import { useNavigate, useLocation } from "react-router-dom";
import { loginUser } from "../api";
export default function Login() {
const [loginFormData, setLoginFormData] = React.useState({
email: "",
password: "",
});
const [status, setStatus] = React.useState("idle");
const [error, setError] = React.useState(null);
const location = useLocation();
const navigate = useNavigate();
const from = location.state?.from || "/host";
function handleSubmit(e) {
e.preventDefault();
setStatus("submitting");
setError(null);
loginUser(loginFormData)
.then((data) => {
localStorage.setItem("loggedin", true);
navigate(from, { replace: true });
})
.catch((err) => {
setError(err);
})
.finally(() => {
setStatus("idle");
});
}
function handleChange(e) {
const { name, value } = e.target;
setLoginFormData((prev) => ({
...prev,
[name]: value,
}));
}
return (
<div className="login-container">
{location.state?.message && (
<h3 className="login-error">{location.state.message}</h3>
)}
<h1>Sign in to your account</h1>
{error && <h3 className="login-error">{error.message}</h3>}
<form onSubmit={handleSubmit} className="login-form">
<input
name="email"
onChange={handleChange}
type="email"
placeholder="Email address"
value={loginFormData.email}
/>
<input
name="password"
onChange={handleChange}
type="password"
placeholder="Password"
value={loginFormData.password}
/>
<button disabled={status === "submitting"}>
{status === "submitting" ? "Logging in..." : "Log in"}
</button>
</form>
</div>
);
}
Login.jsx with data layer api 😎😎
import React from "react";
import {
useNavigate,
useNavigation,
useLocation,
useActionData,
Form,
} from "react-router-dom";
import { loginUser } from "../api";
export async function action({ request }) {
const formData = await request.formData();
const email = formData.get("email");
const password = formData.get("password");
try {
const data = await loginUser({ email, password });
localStorage.setItem("loggedin", true);
return data;
} catch (err) {
return {
error: err.message,
};
}
}
export default function Login() {
const data = useActionData();
const location = useLocation();
const navigate = useNavigate();
const navigation = useNavigation();
const from = location.state?.from || "/host";
React.useEffect(() => {
if (data?.token) {
navigate(from, { replace: true });
}
}, [data]);
return (
<div className="login-container">
{location.state?.message && (
<h3 className="login-error">{location.state.message}</h3>
)}
<h1>Sign in to your account</h1>
{data?.error && <h3 className="login-error">{data.error}</h3>}
<Form action="/login" method="post" className="login-form">
<input name="email" type="email" placeholder="Email address" />
<input name="password" type="password" placeholder="Password" />
<button disabled={navigation.state === "submitting"}>
{navigation.state === "submitting" ? "Logging in..." : "Log in"}
</button>
</Form>
</div>
);
}
when using loaders
and actions
it may be harming for user experience.
for example when user wants to see details about some product and clicks a tile which navigates him to product detail page, with loaders
there will be a delay before page will show up because there will be data fetching from the server behind. (loaders will make it that product detail page will render after data is ready because it changes WHEN fetching takes place).
Fortunately useNavigation
hook is a tool that can help us to gather information about current status of navigating through our app.
Login.jsx
import { useNavigation } from "react-router-dom"
export default const Login = () => {
const navigation = useNavigation()
console.log(navigation)
// {state: "idle", location: undefined, formMethod: undefined, formAction: undefined, formEncType: undefined, formData: undefined}
after clicking submit button inside form:
// {state: "submitting", location: {pathname: "/login", search: "", hash: "", state: null, key: "2t7r0wya"}, formMethod: "post", formAction: "/login", formEncType: "application/x-www-form-urlencoded", formData: FormData {}}
// {state: "idle", location: undefined, formMethod: undefined, formAction: undefined, formEncType: undefined, formData: undefined}
we can use navigation.state to let user know what is currently happening with his request.
<Form action="/login" method="post">
<input
type="email"
name="email"
placeholder="Email address"
/>
<br />
<input
type="password"
name="password"
placeholder="Password"
/>
<br />
<button disabled={isSubmitting}>{isSubmitting ? "Logging in..." : "Log in"} </button> //HERE WE ARE DOING CONDITIONAL RENDERING DEPENDING ON THAT STATE
</Form>
}
Note: when a function is an async function it is always returning a PROMISE
so it is an indication that
const weather = await getWeather();getWeather() returns a promise because await can ONLY be used on a function that returns promise.
you can think about
defer
that it means "don't wait for this data to load"
Vans.jsx
import { defer } from "react-router-dom"
import { getVans } from "../../api"
export functin loader() {
return defer({vans: getVans()})
// defer takes an object representing any data you want to have access to in the component.
// the value of the object property should be a promise
}
Await is used to render deferred values with automatic error handling.
Await has a resolve
parameter where we are passing data that we ill need to eventually render in our component.
Vans.jsx
export default Vans = () => {
const dataPromise = useLoaderData(); // returned content from loader function above.
return (
<Await resolve={dataPromise.vans}>
{(vans) => {
vans.map((van) => <h1>{van.name}</h1>);
}}
</Await>
);
};
promise we are passing to resolve prop - from react-router docs
Can either be React elements or a function.
When using a function, the value is provided as the only parameter.
<Await resolve={reviewsPromise}>
{(resolvedReviews) => <Reviews items={resolvedReviews} />}
</Await>
When using React elements, useAsyncValue
will provide the data:
<Await resolve={reviewsPromise}>
<Reviews />
</Await>;
function Reviews() {
const resolvedReviews = useAsyncValue();
return <div>{/* ... */}</div>;
}
expects to be rendered inside of a <React.Suspense> parent to enable the fallback UI.
Vans.jsx
return (
<div className="van-list-container">
<h1>Explore our van options</h1>
<React.Suspense fallback={<h1>Loading vans data</h1>}>
<Await resolve={dataPromise.vans}>{renderVanElements}</Await>
</React.Suspense>
</div>
);
When Await will wait for promise to resolve and actually get the vans data, "Loading vans data" will be displayed in the UI.
HostVans.jsx without react-router-v6 new data layer api
import React from "react";
import { Link } from "react-router-dom";
export default function HostVans() {
const [vans, setVans] = React.useState([]);
React.useEffect(() => {
fetch("/api/host/vans")
.then((res) => res.json())
.then((data) => setVans(data.vans));
}, []);
const hostVansEls = vans.map((van) => (
<Link to={van.id} key={van.id} className="host-van-link-wrapper">
<div className="host-van-single" key={van.id}>
<img src={van.imageUrl} alt={`Photo of ${van.name}`} />
<div className="host-van-info">
<h3>{van.name}</h3>
<p>${van.price}/day</p>
</div>
</div>
</Link>
));
return (
<section>
<h1 className="host-vans-title">Your listed vans</h1>
<div className="host-vans-list">
{vans.length > 0 ? (
<section>{hostVansEls}</section>
) : (
<h2>Loading...</h2>
)}
</div>
</section>
);
}
HostVans.jsx with usage of data layer api
import React from "react";
import { Link, useLoaderData, defer, Await } from "react-router-dom";
import { getHostVans } from "../../api";
export const loader = () => {
return defer({ hostVans: getHostVans() });
};
export default function HostVans() {
const hostVansPromise = useLoaderData();
function renderHostVans(hostVans) {
return hostVans.map((van) => {
return (
<Link to={van.id} key={van.id} className="host-van-link-wrapper">
<div className="host-van-single" key={van.id}>
<img src={van.imageUrl} alt={`Photo of ${van.name}`} />
<div className="host-van-info">
<h3>{van.name}</h3>
<p>${van.price}/day</p>
</div>
</div>
</Link>
);
});
}
return (
<section>
<h1 className="host-vans-title">Your listed vans</h1>
<div className="host-vans-list">
<section>
<React.Suspense fallback={<h1>Loading </h1>}>
<Await resolve={hostVansPromise.hostVans}>{renderHostVans}</Await>
</React.Suspense>
</section>
</div>
</section>
);
}
index.jsx (we have to specify loader for that
Route
)
import HostVans, { loader as hostVansLoader} from "./pages/Host/HostVans"
...
<Route path="vans" element={<HostVans />} loader={hostVansLoader} />
...
api.js (getHostVans fetching function)
export async function getHostVans() {
await sleep(1000); // fake making response longer so suspense fallback is visible
const res = await fetch("/api/host/vans");
if (!res.ok) {
throw {
message: "Failed to fetch vans",
statusText: res.statusText,
status: res.status,
};
}
const data = await res.json();
return data.vans;
}