Error triggering a Link hook using react-router-v6 - reactjs

I'm updating a sourcecode from react-router-5 to version 6. So far I'm getting this error:
Error: [div] is not a <Route> component. All component children of <Routes> must be a <Route> or <React.Fragment>
The bug is triggered when I activate a link in this component(QuoteItem.js):
const QuoteItem = (props) => {
return (
<li className={classes.item}>
<figure>
<blockquote>
<p>{props.text}</p>
</blockquote>
<figcaption>{props.author}</figcaption>
</figure>
<div className={classes.item}>
<Link className='btn' to={`/quotes/${props.id}`}>View Fullscreen</Link>
</div>
</li>
);
};
in another component (VerQuotes) I defined the routes:
const VerQuotes = () => {
return (
<div>
<main>
<Layout>
<Routes>
<Route path="quotes" element={<AllQuotes />} />
<Route path="new-quote" element={<NewQuote />} />
<Route path="quotes/:quoteId" element={<QuoteDetail />} />
</Routes>
</Layout>
</main>
</div>
);
};
I'm kind of lost how to tackle the error, your comments will be highly appreciated.
Thanks a lot
Update
QuoteDetail
const QuoteDetail = () => {
const match = useNavigate();
const params = useParams();
const { quoteId } = params;
const { sendRequest, status, data: loadedQuote, error } = useHttp(getSingleQuote, true);
//const quote = DUMMY_NOTES.find((quote) => quote.id === params.quoteId);
useEffect(() => {
sendRequest(quoteId);
}, [sendRequest, quoteId]);
if(status === "pending"){
return (
<div className="centered">
<LoadingSpinner />
</div>
);
}
if(error){
return <p className="centered">{error}</p>;
}
if (!loadedQuote.text) {
return <p>No Quote Found!</p>;
}
return (
<Fragment>
<HighlightedQuote text={loadedQuote.text} author={loadedQuote.author} />
<Routes>
<Route path={match}>
<div className="centered">
<Link
className="btn--flat"
to={`${match}/comments`}
>
Load Comments
</Link>
</div>
</Route>
<Route path={`${match}/comments`} element={<Comments />}></Route>
</Routes>
</Fragment>
);
};

Issues
After tracing your code I found you had a couple issues in QuoteDetail component.
You used const match = useNavigate(); (so match is really the navigate function) but then later used match to attempt to form a path string for a Route.
The Route component's children prop is only for rendering nested routes. The error you see is the use of the div element that isn't a Route component.
Code
<Routes>
<Route path={match}>
<div className="centered">
<Link
className="btn--flat"
to={`${match}/comments`}
>
Load Comments
</Link>
</div>
</Route>
<Route path={`${match}/comments`} element={<Comments />}></Route>
</Routes>
Solution
Remove const match = useNavigate(); since it is not used, and place the div into the element prop of the Route. Change the path props to use relative routing from the current route path that's already been built up.
const QuoteDetail = () => {
const params = useParams();
const { quoteId } = params;
const { sendRequest, status, data: loadedQuote, error } = useHttp(
getSingleQuote,
true
);
//const quote = DUMMY_NOTES.find((quote) => quote.id === params.quoteId);
useEffect(() => {
sendRequest(quoteId);
}, [sendRequest, quoteId]);
if (status === "pending") {
return (
<div className="centered">
<LoadingSpinner />
</div>
);
}
if (error) {
return <p className="centered">{error}</p>;
}
if (!loadedQuote.text) {
return <p>No Quote Found!</p>;
}
return (
<Fragment>
<HighlightedQuote text={loadedQuote.text} author={loadedQuote.author} />
<Routes>
<Route
path="/"
element={
<div className="centered">
<Link className="btn--flat" to="comments">
Load Comments
</Link>
</div>
}
/>
<Route path="comments" element={<Comments />} />
</Routes>
</Fragment>
);
};

Related

React - display specific content based on URL using useLocation

Trying to teach myself react and stuck on one part... I can't seem to get page specific content to display based on URL using useLocation() -- HELP!
App.js - router displays page on click, yay!
<Router>
<Routes>
<Route exact path="/" element={<Home />} />
<Route path="/project/projectOne" element={<Project />} />
<Route path="/project/projectTwo" element={<Project />} />
</Routes>
</Router>
Project.js - Project template serves up the components as expected
const Project = () => {
return (
<div className='content-wrapper'>
<Scroll />
<ProjectIntro />
<ProjectContent />
<ProjectGrid />
<Contact />
</div>
); }; export default Project;
ProjectIntro.js - A component trying to serve up the content -- this is where I'm stuck, useLocation() see's the path, but I can't figure out how to show the "projectIntroDetails" based on that path.
const projectOne = () => {
<h1 className='project-intro-heading'>Title Here</h1>,
<figure className='project-intro-image'>
<img src={projectImage} alt='placeholder'/>
</figure>
}
const projectTwo = () => {
<h1 className='project-intro-heading'>Title Here</h1>,
<figure className='project-intro-image'>
<img src={projectTwoImage} alt='placeholder' />
</figure>
}
const projectIntroDetails = {
projectOne: {
component: <projectOne />
},
projectTwo: {
component: <projectTwo />
}
}
const ProjectIntro = () => {
const projectPath = useLocation();
console.log(projectPath);
// this is where I need help
// how do I turn the path into seeing details to render the correct content?
const projectIntroDetail = projectIntroDetails[projectPath.pathname.split("/project/")];
return (
<div className='project-intro'>
{projectIntroDetail}
</div>
);
}; export default ProjectIntro;
You can use a component with a switch statement to determine which child component to render. This method allows you to pass any additional props to the child components.
If you don't need the <div className='project-intro'> element, you could also render the switch directly inside your ProjectIntro component.
const ProjectOne = () => {
<h1 className='project-intro-heading'>Title Here</h1>,
<figure className='project-intro-image'>
<img src={projectImage} alt='placeholder'/>
</figure>
}
const ProjectTwo = () => {
<h1 className='project-intro-heading'>Title Here</h1>,
<figure className='project-intro-image'>
<img src={projectTwoImage} alt='placeholder' />
</figure>
}
const ProjectIntros = ({ slug, ...props }) => {
switch(slug) {
case 'projectOne':
return <ProjectOne {...props} />;
case 'projectTwo':
return <ProjectTwo {...props} />;
default:
return null;
}
}
const ProjectIntro = () => {
const projectPath = useLocation();
console.log(projectPath);
return (
<div className='project-intro'>
<ProjectIntros slug={projectPath.pathname.split("/")[2]} />
</div>
);
}; export default ProjectIntro;
You don't really need to use the useLocation hook or pathname value to handle any conditional rendering logic, that's what the routing components are for.
I would suggest either passing in the correct sub-project component as a prop to be rendered on the correctly matching route, or refactoring the routes to do this in a more "react router" way.
Passing component down as prop example:
App
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route
path="/project/projectOne"
element={<Project projectIntro={<ProjectOne />} />}
/>
<Route
path="/project/projectTwo"
element={<Project projectIntro={<ProjectTwo />} />}
/>
</Routes>
</Router>
Project
const Project = ({ projectIntro }) => {
return (
<div className='content-wrapper'>
<Scroll />
<div className='project-intro'>
{projectIntro}
</div>
<ProjectContent />
<ProjectGrid />
<Contact />
</div>
);
};
Using react-router-dom to your advantage.
Project
Convert Project into a layout component and render the ProjectOne and ProjectTwo components on nested routes. Layout routes are intended to be used to share common UI elements and layout, and render routed content into an outlet.
import { Outlet } from 'react-router-dom';
const Project = () => {
return (
<div className='content-wrapper'>
<Scroll />
<div className='project-intro'>
<Outlet /> // <-- render nested routes here
</div>
<ProjectContent />
<ProjectGrid />
<Contact />
</div>
);
};
App
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/project" element={<Project />}>
<Route path="projectOne" element={<ProjectOne />} />
<Route path="projectTwo" element={<ProjectTwo />} />
</Route>
</Routes>
</Router>

Private Route is not rendering About Component

so I have the private routes set up in App.js. The other components in the private routes render correctly. The About Component isnt rendering. Before I was getting an error saying something along the lines of expected a string but got an object. Now I can goto the about page and the error is gone. I console.log props and slides but it doesnt show up in the console. I am passing props(slides) in the Private route to About.js.
Hi. Ive been stuck on this for two days now. PRivate route doesnt show the About component. It works on all other components. Code is below.Any help is greatly appreciated.
function App() {
return (
<div className="App">
<Nav/>
<main>
<Switch>
<PrivateRoute exact path="/home" component={Home} />
<PrivateRoute path="/resources" component={Resources} />
<PrivateRoute path = "/about" component={ About} slides= {SliderData} />
<PrivateRoute path="/mealplan" component={MealPlan} />
</Switch>
<Route exact path="/" component={SignUp} />
<Route path="/login" component={Login} />
</main>
</div>
);
}
export default App;
function About(slides) {
const [current, setCurrent] = useState(0);
const length = slides.length
if (!Array.isArray(slides) || slides.length <= 0) {
return null;
}
const nextSlide = () => {
setCurrent(current === length - 1 ? 0 : current + 1);
};
const prevSlide = () => {
setCurrent(current === 0 ? length - 1 : current - 1);
};
return (
<>
<section className="slider">
<FaArrowAltCircleLeft onClick={prevSlide} className="left-arrow" />
<FaArrowAltCircleRight onClick={nextSlide} className="right-arrow" />
{SliderData.map((slide, index) => {
return (
<div className={index === current ? "slide-active" : "active"} key={index}
>
{index === current && (
<img src={slide.image} alt="dog" className="dog-img" />
)}
</div>
);
})}
</section>
</>
Private Route
const PrivateRoute = ({component: Component, ...rest}) => {
return(<Route {...rest} render={
(props) => {
if (localStorage.getItem("token")) {
return <Component {...props}/>;
} else {
return(<Redirect to='/login'/>);
}
}
}/>);
};
Like I said it works on the other components but not the About Component. I have tried everything I can think of but cant get it to render
The issue is that you are trying to pass additional props through PrivateRoute to the component but don't pass them all the way through. You only pass the route props through.
PrivateRoute
const PrivateRoute = ({ component: Component, ...rest }) => {
return(
<Route
{...rest}
render={
(props) => {
if (localStorage.getItem("token")) {
return <Component {...props}/>; // <-- only route props from `render`
} else {
return(<Redirect to='/login'/>);
}
}}
/>
);
};
Refactor the PrivateRoute so it renders more like a plain Route component.
const PrivateRoute = (props) => {
return localStorage.getItem("token") ? (
<Route {...props} />
) : (
<Redirect to='/login'/>
);
};
Now use the render prop to render the About component and pass the additional prop(s) through.
function App() {
return (
<div className="App">
<Nav/>
<main>
<Switch>
<PrivateRoute exact path="/home" component={Home} />
<PrivateRoute path="/resources" component={Resources} />
<PrivateRoute
path="/about"
render={props => (
<About {...props} slides= {SliderData} />
)}
/>
<PrivateRoute path="/mealplan" component={MealPlan} />
</Switch>
<Route exact path="/" component={SignUp} />
<Route path="/login" component={Login} />
</main>
</div>
);
}
Ensure you are accessing the slides prop correctly. I.E. props.slides.
function About({ slides }) {
const [current, setCurrent] = useState(0);
const length = slides.length
if (!Array.isArray(slides) || slides.length <= 0) {
return null;
}
const nextSlide = () => {
setCurrent(current === length - 1 ? 0 : current + 1);
};
const prevSlide = () => {
setCurrent(current === 0 ? length - 1 : current - 1);
};
return (
<>
<section className="slider">
<FaArrowAltCircleLeft onClick={prevSlide} className="left-arrow" />
<FaArrowAltCircleRight onClick={nextSlide} className="right-arrow" />
{SliderData.map((slide, index) => {
return (
<div className={index === current ? "slide-active" : "active"} key={index}
>
{index === current && (
<img src={slide.image} alt="dog" className="dog-img" />
)}
</div>
);
})}
</section>
</>
);
}

Is it possible to use multiple outlets in a component in React-Router V6

I am using React Router v6 in an application. I have a layout page, which uses an outlet to then show the main content. I would also like to include a title section that changes based on which path has been matched, but I am unsure how to do this.
function MainContent() {
return (
<div>
<div>{TITLE SHOULD GO HERE}</div>
<div><Outlet /></div>
</div>
);
}
function MainApp() {
return (
<Router>
<Routes>
<Route path="/projects" element={<MainContent />} >
<Route index element={<ProjectList />} title="Projects" />
<Route path="create" element={<CreateProject />} title="Create Project" />
</Route>
<Routes/>
</Router>
);
}
Is something like this possible? Ideally, I would like to have a few other props besides title that I can control in this way, so a good organization system for changes like this would be great.
The most straightforward way would be to move the title prop to the MainContent layout wrapper and wrap each route individually, but you'll lose the nested routing.
An alternative could be to create a React context to hold a title state and use a wrapper component to set the title.
const TitleContext = createContext({
title: "",
setTitle: () => {}
});
const useTitle = () => useContext(TitleContext);
const TitleProvider = ({ children }) => {
const [title, setTitle] = useState("");
return (
<TitleContext.Provider value={{ title, setTitle }}>
{children}
</TitleContext.Provider>
);
};
Wrap the app (or any ancestor component higher than the Routes component) with the provider.
<TitleProvider>
<App />
</TitleProvider>
Update MainContent to access the useTitle hook to get the current title value and render it.
function MainContent() {
const { title } = useTitle();
return (
<div>
<h1>{title}</h1>
<div>
<Outlet />
</div>
</div>
);
}
The TitleWrapper component.
const TitleWrapper = ({ children, title }) => {
const { setTitle } = useTitle();
useEffect(() => {
setTitle(title);
}, [setTitle, title]);
return children;
};
And update the routed components to be wrapped in a TitleWrapper component, passing the title prop here.
<Route path="/projects" element={<MainContent />}>
<Route
index
element={
<TitleWrapper title="Projects">
<ProjectList />
</TitleWrapper>
}
/>
<Route
path="create"
element={
<TitleWrapper title="Create Project">
<CreateProject />
</TitleWrapper>
}
/>
</Route>
In this way, MainContent can be thought of as UI common to a set of routes whereas TitleWrapper (you can choose a more fitting name) can be thought of as UI specific to a route.
Update
I had forgotten about the Outlet component providing its own React Context. This becomes a little more trivial. Thanks #LIIT.
Example:
import { useOutletContext } from 'react-router-dom';
const useTitle = (title) => {
const { setTitle } = useOutletContext();
useEffect(() => {
setTitle(title);
}, [setTitle, title]);
};
...
function MainContent() {
const [title, setTitle] = useState("");
return (
<div>
<h1>{title}</h1>
<div>
<Outlet context={{ title, setTitle }} />
</div>
</div>
);
}
...
const CreateProject = ({ title }) => {
useTitle(title);
return ...;
};
...
<Router>
<Routes>
<Route path="/projects" element={<MainContent />}>
<Route index element={<ProjectList title="Projects" />} />
<Route
path="create"
element={<CreateProject title="Create Project" />}
/>
</Route>
</Routes>
</Router>
I was facing the same issue for a left-right layout: changing sidebar content and main content, without repeating styling, banner, etc.
The simplest approach I found was to remove nested routing, and create a layout component in which I feed the changing content through properties.
Layout component (stripped for this post):
export function Layout(props) {
return (
<>
<div class="left-sidebar">
<img id="logo" src={Logo} alt="My logo" />
{props.left}
</div>
<div className='right'>
<header className="App-header">
<h1>This is big text!</h1>
</header>
<nav>
<NavLink to="/a">A</NavLink>
|
<NavLink to="/b">B</NavLink>
</nav>
<main>
{props.right}
</main>
</div>
</>
);
}
Usage in react router:
<Route path="myPath" element={
<Layout left={<p>I'm left</p>}
right={<p>I'm right</p>} />
} />
Another solution is to use the handle prop on the route as described in the useMatches documentation.
import { useMatches } from "react-router-dom";
function MainContent() {
const matches = useMatches()
const [title] = matches
.filter((match) => Boolean(match.handle?.title))
.map((match) => match.handle.title);
return (
<div>
<div>{title}</div>
<div><Outlet /></div>
</div>
);
}
function MainApp() {
return (
<Router>
<Routes>
<Route path="/projects" element={<MainContent />} >
<Route index element={<ProjectList />} handle={{ title: "Projects" }} />
<Route path="create" element={<CreateProject />} handle={{ title: "Create Project" }} />
</Route>
<Routes/>
</Router>
);
}

From static pagination to dynamic

Need help with pagination. Right now my app can change page, but if I want send request like /character?page=4 it always throw me /character?page=1 this is not help, coz I use router. I have no idea how to resolve my problem
My app.js
function App() {
return (
<>
<Router>
<Header />
<Switch>
<Route exact path='/' component={Home} />
<Route exact path='/character' component={AllCharacters} />
<Route component={NotFound} />
</Switch>
<Footer />
</Router>
</>
);
}
CharacterList.jsx
// API Data
const url = "https://rickandmortyapi.com/api/character";
// Fetching Page
const fetchPage = (page) => {
// Init loading while page load
setLoading(true);
const query = `${url}?page=${page}`;
fetchData(query);
setLoading(false);
};
// Change pages
const { push } = useHistory();
const handleChanger = (event, page) => {
fetchPage(page);
push({
pathname: "/character",
search: `?page=${page}`,
});
};
<Pagination
count={info.pages}
showLastButton
showFirstButton
onChange={handleChanger}
/>
Dynamic Pagination : App.js
function App() {
return (
<>
<Router>
<Header />
<Switch>
<Route exact path='/' component={Home} />
<Route exact path='/character/page=:page' component={DynamicPagination} />
<Route component={NotFound} />
</Switch>
<Footer />
</Router>
</>
);
}
Dynamic pagination Demo : DynamicPaginaton.js
export default function DynamicPagination() {
const history = useHistory();
const [page, setPage] = React.useState(0);
const [pageCount, setPagcount] = useState(10);
const handlePageChange = (event, value) => {
history.push(`/character/page=${value}`)
setPage(value);
};
return (
<div >
<Typography>page:{page}</Typography>
<Pagination
count={pageCount}
page={page}
onChange={handlePageChange}
style={{ marginTop: 20 }}
/>
</div>
);
}

Infinite Loop in Reach Router

I'm a bit new to React and it is my first time using reach-router (or any kind of router really). What I'm trying to do is have a nested component inside one of my router links. Basically, within my ItemShop component, I want to have two more links to components (both of which are defined within my ItemShop component), and I want to display whichever component is selected under the navbar. It seems similar to something they do in the tutorial, but for some reason I seem to get an infinite loop when I click on a link.
Here is my top-level router, in App.js:
function App() {
return (
<div>
<Router>
<HomePage path="/" />
<ItemShop path="ItemShop" />
<Item path="ItemShop/:id" />
<Challenge path="Challenge" />
<Achievements path="Achievements" />
<BattlePass path="BattlePass" />
<Miscellaneous path="Miscellaneous" />
</Router>
</div>
);
}
And this is my ItemShop component where I'm trying to render the links, ItemShop.js:
render() {
// ... assigning arrays here
let Current = () => ( //...);
let Upcoming = () => ( //...);
return(
<>
<div className="nav-container">
<Navbar />
</div>
//...
<div>
<nav className="side-nav">
<Link to="/current">Current</Link>{" "}
<Link to="/upcoming">Upcoming</Link>
</nav>
<Router>
<Current path="current" />
<Upcoming path="upcoming" />
</Router>
</div>
//...
{this.props.children}
)
}
}
Again I am very new to Javascript/React as a whole, so it could just be a fundamental flaw. I have already sunk quite a few hours into this so I would really appreciate some guidance. Thank you for your time!
I tried using React-Router-Dom instead of reach-router. I made it so it renders both <Upcoming /> and <Current /> components inside of the <ItemShop /> component. You can check it out how I have done it below. I hope this helps.
// import React from "react";
// import { BrowserRouter as Router, Route, Switch, Link } from "react-router-dom";
export default function App() {
return (
<div className="App">
<Router>
<Switch>
<Route exact path="/" component={HomePage} />
<Route path="/itemShop" component={ItemShop} />
<Route path="/itemShop/:id" component={Item} />
<Route path="/challenge" component={Challenge} />
<Route path="/achievements" component={Achievements} />
<Route path="/battlePass" component={BattlePass} />
<Route path="/miscellaneous" component={Miscellaneous} />
</Switch>
</Router>
</div>
);
}
const HomePage = () => {
return <div>Home Page</div>;
};
const ItemShop = () => {
const Current = () => {
return <div>Current</div>;
};
const Upcoming = () => {
return <div>Upcoming</div>;
};
return (
<div>
<div>Item Shop</div>
<Link to="/itemShop/current">Current</Link>{" "}
<Link to="/itemShop/upcoming">Upcoming</Link>
<br />
<br />
<Route
render={() =>
window.location.pathname === `/itemShop/current` ? (
<Current />
) : (
<Upcoming />
)
}
/>
</div>
);
};
const Item = () => {
return <div>Item</div>;
};
const Challenge = () => {
return <div>Challenge</div>;
};
const Achievements = () => {
return <div>Achievements</div>;
};
const BattlePass = () => {
return <div>BattlePass</div>;
};
const Miscellaneous = () => {
return <div>Miscellaneous</div>;
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-router-dom/6.0.0-beta.0/react-router-dom.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

Resources