30 Days of React: Day 17 | React Router
Click for Table of Contents
-
Since the original source material is outdated (react-router-dom@4) and the current version is at v6, an external source material is used for this sub-repo.
-
Created a CustomNavLink based on code snippet from react-router official docs.
-
CSS reset - version 1.7.3 by @elad2412 used
-
included experimental chromiumAutofill.css [w/ notes & attribution] to mask autofill style in chromium. (Default style cannot be reset/sanitize and can only be masked or delay effect)
-
used experimental :has() selector in navigation.module.css
/* apply declaration block to div if it contains a ".sideNav" element (note: .sideNav has float: left; declaration) */ div:has(.sideNav) { display: flow-root; }
-
added
getTimestamp
function in utils/misc.js to help with time-tracking -
started returning a cleanup function to ensure that enclosed function in
useEffect
is ran only onceBased on observation:
- the returned cleanup function catches up with the operation from the 1st run of
useEffect
, then cancels that operation - the operation from the 2nd run of
useEffect
persists and is used to update relevant data
Code snippet from NavigateSample component
/* pages/Navigate.js */ useEffect(() => { const interval = setInterval(() => setCountdown(prev => --prev), 1000) /* cleanup function */ return () => clearInterval(interval) }, [])
Code snippet from Level1 component
/* pages/Exercises/Level1.js */ useEffect(() => { const URL = '../data/level1qna.json' const controller = new AbortController() const fetchData = async () => { try { const res = await fetch(URL, { signal: controller.signal }) const data = await res.json() setQna(prev => data) } catch (err) { console.log(err) } } fetchData() /* cleanup function */ return () => controller.abort('Run only once') }, [])
- the returned cleanup function catches up with the operation from the 1st run of
/* index.js */
<BrowserRouter>
<App />
</BrowserRouter>
/* App.js */
<Container>
<NavigationComponent />
<RoutesLogic />
</Container>
Code structure in
App.js
can be replicated at lower levels (not root) but with the explicit rule of the parentRoute
path
having trailing/*
-- to connect/relate it with its childRoutes
(ie.<RoutesLogic />
)Example:
/* parent `Route` */ { path: 'blog/*', /* notice trailing marker */ element: <BlogRoutesLogic /> }/* child `Routes` || `<RoutesLogic />` */ { path: '/', /* this line can be removed */ children: [ { index: true, element: <Blog /> }, { path: '2022-10-13', element: <Blog20221013 /> }, ] }
<Route path="/">
<Route index element={<Home />} />
<Route path="contact" element={<ContactMe />} />
</Route>
Can be viewed by going to {baseURL}/challenges/:id
Code snippet from OtherChallenges component
const { id } = useParams()
/* no argument required in useParams */
Code snippet from routes/NavRoutes.js
/* routes/NavRoutes.js */
<Routes>
<Route path="/">
{/* Routes below (with absolute path)
have more priority than Dynamic Route */}
<Route path="1" element={<Challenge1 />} />
<Route path="2" element={<Challenge2 />} />
{/* Dynamic Route */}
<Route path=":id" element={<OtherChallenges />} />
{/* ... rest of code */}
</Route>
</Routes>
Allow multiple Routes
to be rendered at the same time
WARNING
- Try to avoid and use layout with
Context
instead- routes/BlogRoutes.js which is used as an example in this section is currently NOT being imported in the main navigation file: routes/NavRoutes.
- The code sample works when navigating through the UI, however, a console warning is displayed when current location does not match the specified path
# Console Warning No routes matched location "/[insert current pathName if not root path]"
/* routes/BlogRoutes.js */
{/* Pass '/blog' to `location` props of `Routes` ie. `<Routes location="/blog">` to show `<BlogNav />` in all pages */}
<Routes>
{/* display BlogNav only if in path /blog, /blog/1, /blog/2 etc */}
<Route path="/blog/*" element={<BlogNav />} />
</Routes>
To help clean up code, especially if there are many Route
s, Route
s that have a similar path
can be placed in a different file and imported in main <Routes>
.
Code snippet from routes/UpdatesRoutes.js
/* routes/UpdatesRoutes.js */
{/* NOTICE that the child `<Route>`s have relative paths to "updates"
This is because the PARENT path is declared in the main <Route> in NavRoutes.js */}
<Routes>
<Route element={<UpdatesLayout />}>
<Route index element={<Updates />} />
<Route path="1" element={<Updates1/>} />
<Route path="2" element={<Updates2/>} />
</Route>
</Routes>
Code snippet from routes/NavRoutes.js
/* routes/NavRoutes.js */
{/* NOTICE how path is written with trailing "/*"
this is to cover other paths contained in "updates"
that are being imported from routes/UpdatesRoutes.js */}
<Route path="updates/*" element={<UpdatesRoutes />} />
Instead of using JSX, an array of Javascript Objects representing the logic of what element to display/render can be used as an argument to useRoutes()
hook. Using this method can remove repetition like writing the template for Route
ie. <Route />
Code snippet from useRoutes/routes/URoutes.js
/* URoutes.js */
const URoutes = () => {
const element = useRoutes([
{
path: '/useRoutes', /* <Route path='/useRoutes'> */
children: [
{
index: true, /* <Route index */
element: <URHome />, /* element={<URHome />} /> */
},
{
path: 'contact', /* <Route path='contact' */
element: <URContact />, /* element={<URContact/>} /> */
},
]
} /* </Route> */
])
return element
}
Use path="*"
for any routes/page that do not match the existing declared routes
{/* At the same level as path="/" or one level below if nested inside
Unless a different and more specific NotFound element is needed */}
<Route path="*" element={<NotFound />} />
- Layouts can be shared by passing a layout component to parent
Route
'selement
. - Ensure that the layout component has an
<Outlet>
inside that will serve as a placeholder for thechildren
<Route>
s.
Code snippet from layouts/ChallengesLayout.js
/* layouts/ChallengesLayout.js */
import { Outlet } from "react-router-dom"
const ChallengesLayout = () => {
/* ... some code */
return (
<>
{/* ... some JSX elements and code */}
<Outlet />
{/* ... some JSX elements and code */}
</>
)
}
Code snippet from routes/navRoutes.js
/* routes/NavRoutes.js */
{/* ChallengesLayout passed as element on parent route to display layout */}
<Route path="challenges" element={<ChallengesLayout />}>
<Route index element={<Challenges />} />
<Route path="1" element={<Challenge1 />} />
<Route path="2" element={<Challenge2 />} />
<Route path=":id" element={<OtherChallenges />} />
</Route>
Nested Route
's that have a common path can share a layout by passing the layout component to the Parent Route
's element
attribute.
Code snippet from routes/NavRoutes.js
<Route path="challenges" element={<ChallengesLayout />}>
<Route index element={<Challenges />} />
<Route path="1" element={<Challenge1 />} />
<Route path="2" element={<Challenge2 />} />
<Route path=":id" element={<OtherChallenges />} />
</Route>
Sharing a layout between Route
's that have NO common path can be done by wrapping the Routes in a Parent Route
that has NO PATH with the layout component as the element
value.
Code snippet from routes/NavRoutes.js
{/* PARENT `Route` should not have a path property because the enclosed `Route`s do not share a common path */}
<Route element={<SharedLayout />}>
<Route path="/about" element={<About />} />
<Route path="/contact">
<Route index element={<Contact />} />
<Route path=":contactId" element={<SomeContact />} />
{/* experimenting with :id */}
<Route path=":contactId/:contactId" element={<SomeOtherContact />} />
</Route>
</Route>
context
property allows data to be passed from layout component whereOutlet
is declared.Code snippet from layouts/ChallengesLayout.js
/* layouts/ChallengesLayout.js */ <Outlet context={{ name, click }} />
useOutletContext
hook is used to extract thecontext
object fromOutlet
.Code snippet from pages/Challenges.js
/* pages/Challenges.js */ const FromOutlet = () => { const { name, click } = useOutletContext() return ( <div> <h6>From Context</h6> <p>Name: {name}</p> <p>Click: {click}</p> </div> ) }
Link
is the simplest form of navigation. Underneath, this is a simple anchor tag.
Below are the properties available in <Link>
Accepts the path where to redirect. The path can either be one of the ff:
- absolute path
- relative path
- appending the path to the current path displayed
- using directory-like navigation eg.
./
or../
Code snippet from navigation/ChallengesNav.js
/* navigation/ChallengesNav.js */
{/* //> Link to absolute path */}
<li><Link to="/challenges/1">Challenge 1</Link></li>
{/* //> Link to relative path */}
<li><Link to="2">Challenge 2</Link></li>
{/* //> Link to relative path using directory-lik navigation */}
<li><Link to='./'>Challenges main</Link></li>
<li><Link to='../'>Back to home</Link></li>
replace
is a boolean property that when present (default value is true
) will replace the previous path -- after clicking <Link>
-- in memory. See example below.
# history before clicking <Link>
/ > /challenges > /challenges/1
# history after clicking <Link> going to /challenges/2
# <Link to='/challenges/2' replace>Challenge 2</Link>
/ > /challenges > /challenges/2
# notice that /challenges/2 replaced /challenges/1 in history
# this means that clicking on the back button will direct the page to /challenges
reloadDocument
is a boolean property that when present (default value is true
) will reload the entire document.
Note that reload will reset
state
s not stored (eg. localStorage, sessionStorage, cache, etc.)
state
can be passed from one path to another and can be accessed usinguseLocation
const location = useLocation()
useLocation
hook returns a location object (based on the URL){ hash: "", key: "", pathname: "/", search: "", state: null }
NavLink
is similar to Link
with more customization.
All properties available in
<Link>
is available in<NavLink>
plusend
end
is a boolean property that when present (default value is true
) will only apply the 'active' className if the current path is exactly as what's specified in the to
property.
Common use:
<NavLink to='/' end> Home </NavLink>If above not used, navigating to any other path will not remove the
active
className inHome
resulting to multipleNavLink
appearing as 'active'
There are multiple ways of styling active
NavLink
s from inline styles to using classNames creatively, and even creating a CustomNavLink Refer to navigation/MainNav.js for different styling used
Navigate
is a component that when rendered will automatically redirect the pageuseNavigate
hook returns a function that can be used to redirect pageconst navigate = useNavigate() /** * @params to: To ("pathName"), options?: {replace: boolean, state: any} * @params delta: number * ! if delta param used, options?: {state: any} will not be passed This is because state is tied in with `location` TRY: `const location = useLocation()` and console.log for more visual */ navigate('/pathName', {})
- function from
useNavigate()
accepts 2 parameters:to
which is the path where to navigate to ordelta
which is a numerical representation of the location to go to based on history (eg.-1
for going 1 location back)- an object that stores
replace
: boolean and/orstate
- function from
to replace reloadDocument state end Link ✅ ✅ ✅ ✅ ◼️ NavLink ✅ ✅ ✅ ✅ ✅ Navigate 1 ✅ ✅ ◼️ ✅ ◼️
It is possible to pass data using react-router-dom
as seen in previous sections. Here is a quick recap on how data can be passed.
useParams
in Dynamic Routing / useParams Hook -- id/endpoint can be passed to Dynamic Component. egconst { id } = useParams()
useOutletContext
in Context / useOutletContext Hook -- data from Layout can be passed to outlet by passing data (type: any) tocontext
props ofOutlet
Link
,NavLink
andNavigate
/useNavigate
in Navigation -- data can be maid available to next location/route by passing data to component'sstate
props oroptions?
param of function generated fromuseNavigate
. egnavigate('/', { replace, state })
.state
can be retrieved in new location by usinguseLocation
. Look back in state --> useLocation HookuseSearchParams
which looks similar touseState
allows query to be displayed in the URLCode snippet from layouts/LiveSearchLayout.js
const LiveSearchLayout = () => { const [searchParams, setSearchParams] = useSearchParams({ q: '' }) const [resData, setResData] = useState({ prop1: '', prop2: '', }) const search = searchParams.get('q') const handleSearchChange = (e) => { const { value } = e.target setSearchParams(prev => ({ ...prev, q: value })) /* fake data fetching */ const resFromFakeReactQuery = fakeReactQuery(value) setResData(prev => ({...prev, prop1: 'resFromFakeReactQuery', prop2: resFromFakeReactQuery })) } }
- Parent class (style) can be used to style child elements. Example can be found in App.module.css and App.js
/* App.module.css */ .NavStyle { /* ... */ } .NavStyle > ul { /* ... */ } .NavStyle a { /* ... */ } .NavStyle a:hover { /* ... */ }
/* App.js */ import appStyle from './App.module.css' const { NavStyle } = appStyle const HomeNavigation = () => { return ( <nav className={NavStyle}> {/* ... rest of code */}
- Top level element styles have precedence over lower level *element styles
element selector in Parent module takes precedence over Child module
p { color: black; }
inApp.module.css
overridesp { color: red; }
incomponent.module.css
Package | Installation | Website | Github |
---|---|---|---|
react router dom | npm i react-router-dom |
reactrouter.com | remix-run/react-router |
Ultimate React Router v6 Guide by Web Dev Simplified
Read on Github
Footnotes
-
Function from
useNavigate
also shares this. ienavigate
generated fromuseNavigate
↩