Building breadcrumb using React Router V6.4 - javascript

After reading docs about new implementation of v6.4, I came across this new useMatches hook.
I attempt to apply this on a project I'm currently working on and ended up with the following structure.
Routes
export const router = createBrowserRouter(
createRoutesFromElements(
<Route path="/" element={<RootLayout />}>
<Route index element={<TopComponent />} />
<Route
path="path"
element={<TopComponent />}
handle={{
crumb: () => <Link to="/path">path</Link>,
}}
>
<Route
path="path"
element={<ChildComponent />}
handle={{
crumb: () => <Link to="path">Path</Link>,
}}
>
<Route path=":ID" element={<DynamicComponent />}/>
</Route>
<Route
path="path"
element={<ChildComponent />}
handle={{
crumb: () => <Link to="path">Path</Link>,
}}
/>
</Route>
)
TopComponent: Main single nav item
ParentComponent: Nav item with sub-routes
ChildComponent: Nested Component
Breadcrumb Component
import { useMatches } from "react-router-dom";
export function Breadcrumbs() {
let matches = useMatches();
let crumbs = matches
// first get rid of any matches that don't have handle and crumb
.filter((match) => Boolean(match.handle?.crumb))
.map((match) => match.handle.crumb(match.data));
const breadcrumbs = crumbs.map((crumb, ind) => {
const notLast = ind < crumbs.length - 1;
if (notLast) {
return (
<>
<span className="name">{crumb}</span>
<span className="arrow">></span>
</>
);
} else {
return <span className="name">{crumb}</span>;
}
});
return <div>{breadcrumbs}</div>;
}
Result
a tags that enables navigating on clicking
ParentComponent > ChildComponent
Questions
Am I doing things right or there is a better way to construct this?
How to make this method work with dynamic routes?

Related

React Router V5 best way to use context variable in route

In My app I have my routes defined, as per below:
<BrowserRouter>
<Header />
<div className="App">
<Switch>
<Route exact path="/">
<Redirect to="/home" />
</Route>
<Route exact path={["/home", "/"]} component={Home} />
<Route path="/account/:id" render={(props: RouteComponentProps<any>) => <Account {...props} />} />
<Route component={NotFound} />
</Switch>
</div>
</BrowserRouter>
What I want to know is, this can be tricky, If I wanted my route to have a prefix from my context i.e variable how would I do this, but the twist is the variable comes from an api response?
so what if i wanted the route /contextVariable/home but contextVariable is from an api response and is stored in a context value, I know how I would bring that variable into the component but how would the routes handle it i.e from not being /undefined/home as in the response would need to finish before being inserted into the route?
Any idea's?
I had once made a project that had similar requirement. In that, instead of declaring dynamic routes, I fetched a routes array from the state which was an object array with component, path, and few other parameters. By default I added the initial landing page and not found page:
const [routes, setRoutes] = React.useState([
{
component: HomeComponent,
path: '/',
},
{
component: NoMatchPage,
path: '*',
}
])
And then I had the request in a useEffect block which would update this state like so:
React.useEffect(()=>{
// call api()
const oldRoutes = routes;
const noMatchPage = oldRoutes.pop();
const newRoutes = [...oldRoutes,
responseFromApi.map(
routeItem =>
({
component: ComponentName,
path: routeItem.path
})
), noMatchPage]
setRoutes(newRoutes)
},[])
Edit 1 : Cause I'm forgetful
Sorry, I forgot the main part, here's how the Route rendering would be:
<Switch>
{
routes.map(routeItem =>
<Route path={routeItem.path} component={routeItem.component} />
)
}
</Switch>
Also if you want to avoid the extra code in useEffect, you could simply do this:
React.useEffect(()=>{
// call api()
setRoutes(responseFromApi.map(
routeItem =>
({
component: ComponentName,
path: routeItem.path
})
))
},[])
and then
<Switch>
<Route exact path={["/home", "/"]} component={Home} />
{
routes.map(routeItem =>
<Route path={routeItem.path} component={routeItem.component} />
)
}
<Route component={NotFound} />
</Switch>
Edit 2 : Cause I'm ignorant
In the case where the user enters the URL directly and Switch is unable to identify the Route and therefore loads the NotFoundPage, you could do the following:
Set a condition when you start loading your paths, inside your useEffect block:
const [loading, setLoading] = React.useState(false);
React.useEffect(() =>
{
setLoading(true);
// load paths
setLoading(false);
}, [])
While the fetch is in progress, show a Loader to the user:
return
(
<>
{
loading ?
<LoaderComponent /> :
<Switch>
// same as before
</Switch>
}
</>
)
Preferable show something for the user to read, so they don't get irritated, cause patience is a thing of the past. Hope this helps!
If you want to do this with a React Context then this is the pattern I'd suggest. Create a React Context that holds the API logic to fetch a "base path" and expose that out to consumers. Consumers will take the provided "base path" value and prepend it to all link targets and route paths.
Example:
BasePathProvider
import { createContext, useContext } from "react";
const BasePath = createContext({
basepath: ""
});
const BasePathProvider = ({ children }) => {
... logic to fetch basepath ...
return (
<BasePath.Provider value={{ basepath }}>
{children}
</BasePath.Provider>
);
};
const useBasePath = () => useContext(BasePath);
Header
const Header = () => {
const { basepath } = useBasePath();
return (
...
<Link to={`${basepath}/`}>Home</Link>
<Link to={`${basepath}/account/${/* some id value */}`}>
Account
</Link>
...
);
};
App
function App() {
return (
<div className="App">
<Header />
<BasePath.Consumer>
{({ basepath }) => (
<Switch>
<Redirect from={`${basepath}/`} exact to={`${basepath}/home`} />
<Route path={`${basepath}/home`} component={Home} />
<Route path={`${basepath}/account/:id`} component={Account} />
<Route component={NotFound} />
</Switch>
)}
</BasePath.Consumer>
</div>
);
}
index.js
import { BrowserRouter as Router } from "react-router-dom";
import BasePathProvider from "../path/to/BasePathProvider";
...
<Router>
<BasePathProvider>
<App />
</BasePathProvider>
</Router>
Note: You might also want/need to implement a "loading" state to conditionally render the BasePathProvider component's children until the basepath value has been fetched.

props.location is undefined with route component

i'm trying to run snippet code as below:
class App extends Component {
render() {
return (
<Router>
<div className="App">
<Navbar></Navbar>
<Routes>
<Route path="/" element={<Home></Home>} />
<Route path="/about" element={<About></About>} />
<Route path="/contact" element={<Contact></Contact>} />
<Route path="/challenges/*" element={<Challenges></Challenges>} />
<Route path="*" element={<NotFound />} />
</Routes>
</div>
</Router>
);
}
}
let a = 0;
const Challenges = (props) => {
console.log(++a);
console.log(window.location.pathname);
const path = props.location.pathname;
const slug = path.split("/").slice(path.split("/").length - 1)[0];
const challenge = challenges.find((challenge) => challenge.slug === slug);
return (
<div>
<h1>30 Days Of React Challenge</h1>
<ul>
{challenges.map(({ name, slug }) => (
<li key={name}>
<NavLink to={`/challenges/${slug}`}>{name}</NavLink>
</li>
))}
</ul>
<Routes>
<Route
exact
path="/challenges"
element={<h1>Choose any of the challenges</h1>}
/>
<Route path={path} element={<Challenge challenge={challenge} />} />
</Routes>
</div>
);
};
i want to get the path at Challenges route component but it throw an error:
Cannot read properties of undefined (reading 'pathname')
i try to log variable "a" and "window.location" to test and it log two times like this:
1
/challenges
2
/challenges
My question is why i can't take value of props.location.pathname and why its run two times and the second time it throw an error why not at fisrt time.
Thank for helping me! Hope you have a good day.
Issue(s)
react-router-dom v6 Route components rendered via the element prop don't receive route props.
Route children components must use react hooks to access the route context, i.e. useParams, useLocation, useNavigate, etc... and therefore must be function components.
The console.log calls are in the function body so these are unintentional side-effects. This is likely why they are called twice, assuming the app is being rendered into a React.StrictMode component.
Solution
Challenges should use the uselocation hook to access the pathname. Move the console logs into an useEffect hook so they are called once per render to the DOM.
const Challenges = (props) => {
const { pathname } = useLocation();
useEffect(() => {
console.log(++a);
console.log(pathname);
});
const path = pathname;
const slug = path.split("/").slice(path.split("/").length - 1)[0];
const challenge = challenges.find((challenge) => challenge.slug === slug);
return (
<div>
<h1>30 Days Of React Challenge</h1>
<ul>
{challenges.map(({ name, slug }) => (
<li key={name}>
<NavLink to={`/challenges/${slug}`}>{name}</NavLink>
</li>
))}
</ul>
<Routes>
<Route
path="/challenges"
element={<h1>Choose any of the challenges</h1>}
/>
<Route path={path} element={<Challenge challenge={challenge} />} />
</Routes>
</div>
);
};
v6 api-reference

Creating routes inside a private route with react-router v6

Currently using react-router-dom 6.1.1 and I'm working with a private route.
Inside this private route I usually had other routes (so that I can keep my Sidebar on them).
My code looks like this
// App.tsx
const RequireAuth: React.FC<PrivateRouteProps> = ({ children, redirectTo }) => {
const isAuthenticated = Auth.isLogedIn()
return isAuthenticated ? children : <Navigate to={redirectTo} />
}
const Page = () => {
return (
<div className={css.host}>
<BrowserRouter>
<Routes>
<Route path="/login" element={<Login />} />
<Route
path="/"
element={
<RequireAuth redirectTo="/login">
<Home />
</RequireAuth>
}
/>
</Routes>
</BrowserRouter>
</div>
)
}
// Home/index.tsx
const Home = () => {
return (
<div className={css.host}>
<Sidebar sections={sidebarOptions(t)} />
<Routes>
{routes.map(({ breadCrumbtitle, link, component }, index) => (
<Route path={link} key={index}>
{component ? component : <p>[{breadCrumbtitle}] To be done</p>}
</Route>
))}
</Routes>
</div>
)
}
So... This setup worked with v5 but it seems to be something that doesn't really work with v6.
What can I do if I still want to keep the Sidebar for all the routes once I'm logged in?
I ended up finding the solution to my issue.
Doing what Drew Reese suggested only worked to a certain point since I was being led to a route that, for react router, didn't exist.
For it to work I add to do
// App.tsx
const RequireAuth: React.FC<PrivateRouteProps> = ({ children, redirectTo }) => {
const isAuthenticated = Auth.isLogedIn()
return isAuthenticated ? children : <Navigate to={redirectTo} />
}
const Page = () => {
return (
<div className={css.host}>
<BrowserRouter>
<Routes>
<Route path="/login" element={<Login />} />
<Route
path=""
element={
<RequireAuth redirectTo="/login">
<Home />
</RequireAuth>
}
>
{routes.map(({ breadCrumbtitle, link, component }, index) => {
return <Route path={link} key={index} element={component}></Route>
})}
</Route>
</Routes>
</BrowserRouter>
</div>
)
}
// Home/index.tsx
const Home = () => {
return (
<div className={css.host}>
<Sidebar sections={sidebarOptions(t)} />
<div className={css.contentContainer}>
<Outlet />
</div>
</div>
)
}
Using the Outlet seemed to be essential, don't know if it's something new on react router v6 but seemed to do the trick!
As far as I can tell the only issue is with the routes mapping, the Route components have invalid children, i.e. you are rendering another Route or React.Fragment as a child.
Move this up to the element prop of the mapped Route components.
const Home = () => {
return (
<div className={css.host}>
<Sidebar sections={sidebarOptions(t)} />
<Routes>
{routes.map(({ breadCrumbtitle, link, component }, index) => (
<Route
path={link}
key={index}
element={component || <p>[{breadCrumbtitle}] To be done</p>}
/>
))}
</Routes>
</div>
);
};

state do not change with useContext

hello
I am trying to make a menu toggle, where I have a variable with false as initial value, using react createContext and useContext hook, I set the initial state as true
// useMenu Context
import React, { useContext, useState } from 'react'
export const useToggle = (initialState) => {
const [isToggled, setToggle] = useState(initialState)
const toggle = () => setToggle((prevState) => !prevState)
// return [isToggled, toggle];
return { isToggled, setToggle, toggle }
}
const initialState = {
isMenuOpen: true,
toggle: () => {},
}
export const MenuContext = React.createContext(initialState)
const MenuProvider = ({ children }) => {
const { isToggled, setToggle, toggle } = useToggle(false)
const closeMenu = () => setToggle(false)
return (
<MenuContext.Provider
value={{
isMenuOpen: isToggled,
toggleMenu: toggle,
closeMenu,
}}>
{children}
</MenuContext.Provider>
)
}
export default MenuProvider
export const useMenu = () => {
return useContext(MenuContext)
}
so If true it will show the Menu if false it will show the Div where there a div
App.js
const { isMenuOpen } = useMenu()
//the providder
<MenuProvider>
<Header mode={theme} modeFunc={toggleTheme}/>
{isMenuOpen ? (
<Menu />
) : (
<Switch>
<Route path='/writing' component={Writings} />
<Route path='/meta' component={Meta} />
<Route path='/contact' component={Contact} />
<Route path='/project' component={Project} />
<Route exact path='/' component={Home} />
<Route path='*' component={NotFound} />
</Switch>
)}
<Footer />{' '}
</MenuProvider>
and when I add an onclick event the NavLink button of the menu to close it it does not work
Menu
const { closeMenu } = useMenu()
// return statement
{paths.map((item, i) => {
return (
<MenuItem
key={i}
link={item.location}
svg={item.icon}
path={item.name}
command={item.command}
onClick={closeMenu}
/>
)
})}
where did I go wrong
Issue
I suspect the issue is in App where you've a useMenu hook outside the MenuProvider used in App. This useMenu hook is using a MenuContext context but in the absence of a provider it instead uses the default initial context value.
const initialState = {
isMenuOpen: true,
toggle: () => {},
};
export const MenuContext = React.createContext(initialState);
export const useMenu = () => {
return useContext(MenuContext)
};
React.createContext
const MyContext = React.createContext(defaultValue);
Creates a Context object. When React renders a component that
subscribes to this Context object it will read the current context
value from the closest matching Provider above it in the tree.
The defaultValue argument is only used when a component does not
have a matching Provider above it in the tree. This default value can
be helpful for testing components in isolation without wrapping them.
Solution
Since I doubt you want to run/provide more than one menu provider I believe the solution is to move MenuProvider out of and wrap App to provide the context you are updating by nested components.
App.jsx
const { isMenuOpen } = useMenu();
...
<>
<Header mode={theme} modeFunc={toggleTheme}/>
{isMenuOpen ? (
<Menu />
) : (
<Switch>
<Route path='/writing' component={Writings} />
<Route path='/meta' component={Meta} />
<Route path='/contact' component={Contact} />
<Route path='/project' component={Project} />
<Route exact path='/' component={Home} />
<Route path='*' component={NotFound} />
</Switch>
)}
<Footer />
</>
index.jsx (?)
import App from './App.jsx';
...
//the provider
<MenuProvider>
<App />
</MenuProvider>

implement auth route - with nested /signin and /signup routes

I want to implement simple signin/signup routs with react-router-dom.
here is the App.js
<div className="App">
<Switch>
<Route exact path="/" component={HomePage} />
<Route exact path="/auth" component={SignInSignUpPage} />
</Switch>
</div>
and I'm using route guard like so :
const HomePage = ({ currentUser }) => {
return currentUser ? <HomePageComponent /> : <Redirect to="/auth" />;
};
Now what I want to do, is to have the /auth route, for users to login, and inside a container of both the signInSignUp, I have a Link that will change the route to auth/signup to view the signup page like so:
const SignInSignUpPage = ({ match }) => {
return (
<SignInSignUpContainer>
<Route path={`${match.path}`} component={SignIn} />
<Route path={`${match.path}/signup`} component={SignUp} />
</SignInSignUpContainer>
);
};
which in turn will render the correct component :
const SignInSignUpContainer = ({ match, children, history }) => {
const { isExact } = match;
return (
<SignInSignUpContainerContent>
<SignInSignUpContainerContentForm>
<LockIconContainer iconName="icon-lock-closed" />
{children}
</SignInSignUpContainerContentForm>
</SignInSignUpContainerContent>
);
};
I must be doing it wrong, and the react-router-dom docs are addressing the protected route, which I didn't find suitable for this case.
Just from looking at the structure: you don't send match prop down to SignInSignUpContainer, and it seems that SignInSignUpContainer expects it.
should be :
const SignInSignUpPage = ({ match }) => {
return (
<SignInSignUpContainer match={match}>
<Route path={`${match.path}`} component={SignIn} />
<Route path={`${match.path}/signup`} component={SignUp} />
</SignInSignUpContainer>
);
};
same with history prop

Categories