How to render another component in nested route? - reactjs

I use 6.8.1 react-router-dom and react is 18.0.9.
How I can render the page in nested route condition (for example: render the page, MemberExample1 in MemberDetail, once it is clicked)?
Here is how the logic flow. It works until I click and (expected) render the page.
MemberMgt.tsx ->
opened MemberDetail.tsx (from MemberMgt.tsx) ->
Click MemberNavbar's one of button. (Should render the page based on its link and path ON MemberDetail.tsx.)
src > routers.tsx
const Routers = (): JSX.Element => {
const location = useLocation();
return (
<Routes location={location}>
<Route path="/" element={<Main />} />
<Route path="/designsystem" element={<DesignSystem />} />
<Route path="/modal" element={<ModalPage />} />
<Route path="*" element={<NotFound />} />
<Route path="/servermgt" element={<ServerMgt />} />
<Route path="/membermgt">
<Route path="" element={<MemberMgt />} />
<Route path="detail/:id/*">
{/* MemberDetail is the page I should render button when clicked. */}
<Route path="" element={<MemberDetail />} />
</Route>
</Route>
MemberMgt.tsx
src > pages > MemberMgt
return (
// note: viewList is custom hook. This state refers to the custom hook's state.
{_.map(state.viewList, (item, key) => {
<tr
key={key}
onClick={(e) => {
console.log(item.active);
onChoice(item.id);
navigate(`detail/${String(item.data.idx)}`, { state: item.data });
}}
>
MemberDetail.tsx -
This file act as a middle page to display the other page once the button is clicked.
src > pages > MemberMgt> MemberDetail.tsx
import MemberBrief from './MemberDetailMenu/MemberBrief/MemberBrief';
import MemberExample1 from './MemberDetailMenu/MemberMsMed/MemberExample1';
import MemberExample2 from './MemberDetailMenu/MemberPoint/MemberExample2';
import MemberExample3 from './MemberDetailMenu/MemberWarn/MemberExample3';
import MemberNavbar from './MemberDetailMenu/MemberNavbar';
const MemberDetail = () => {
// this state does not refer to the custom hook!
const { state } = useLocation();
return (
<div className="title-box">
<h5>Main - Detail Page</h5>
</div>
<>
<div className="detail-box-detail">
<div className="detail-global-container-gray">
<div className="memberNavbar">
<MemberNavbar />
</div>
{/* nested Route */}
<Routes>
<Route path="*" element={<MemberBrief />} />
{/* <Route path="/brief" element={<MemberBrief />} /> */}
<Route path="/example1" element={<Example1 />} />
<Route path="/example2" element={<Example2 />} />
<Route path="/example3" element={<Example3 />} />
</Routes>
</div>
</div>
</>
)}
Lastly, this is the button, MemberNavbar.tsx.
src > pages > MemberMgt > MemberDetailMenu > MemberNavbar
const MemberNavbar = () => {
const navigate = useNavigate();
const param = useParams();
const handleClickBrief = () => {
navigate(`/membermgt/detail/${param.id as string}`);
};
const handleClickExample1 = () => {
navigate(`/membermgt/detail/${param.id as string}/example1`);
};
const handleClickExample2 = () => {
navigate(`/membermgt/detail/${param.id as string}/example2`);
};
const handleClickExample3 = () => {
navigate(`/membermgt/detail/${param.id as string}/example3`);
};
return (
<div className="detail-button">
{/* <Button style="filled" size="md" onClick={handleInfoClick}> */}
<Button onClick={handleClickBrief} style="transparent" size="md">
Main - detail page
</Button>
<Button onClick={handleClickExample1} style="transparent" size="md">
Example1
</Button>
<Button onClick={handleClickExample2} style="transparent" size="md">
Example2
</Button>
<Button onClick={handleClickExample3} style="transparent" size="md">
Example3
</Button>
</div>
);
};

Related

React Router V5 How to have links to nested and parent router? Adding Redux messes with Routing

I have a page which uses React Router V5. There is a general section and there is a section specifically for user profiles. As I have a different page structure there, I used nested routes for the account section. The SideBar is what the name implies and contains buttons which can be used to navigate the account pages /account/profile, /account/settings, as well as navigate to pages outside the nested switch - namely /help.
The App used to be structured roughly like this:
// in index.js
const appDiv = document.getElementById("app")
render(<App />, appDiv)
// in App.js
const App = () => {
return (
<Router>
<div className={styles.pageContainer}>
<Header />
<Switch>
<Route exact path='/' component={() => <HomePage link='about' />} />
<Route path='/about' component={AboutPage} />
<Route path='/help' component={HelpPage} />
<Route path='/log-in' component={LoginPage} />
<Route path='/sign-up/:signupType' component={SignUpPage} />
<Route path='/account' component={AccountRouter} />
</Switch>
<Footer />
</div>
</Router>
}
// in AccountRouter.js
const AccountRouter = () => {
return (
<div className={styles.container}>
<SideBar />
<Switch>
<Route path='/account/settings' component={AccountSettingsPage} />
<Route path='/account/profile' component={ProfileSettingsPage} />
<Route exact path='/account' component={ProfileSettingsPage} />
</Switch>
</div>
);
};
// in SideBar.js
const SideBar = () => {
const history = useHistory();
return (
<div>
<button onClick={() => history.push('/account/profile')}>Go to Account Profile</button>
<button onClick={() => history.push('/account/settings')}>Go to Account Settings</button>
<button onClick={() => history.push('/help')}>Go to Help Page</button>
</div>
)
}
Now it is structured like this:
// in index.js
const appDiv = document.getElementById("app")
render(
<Provider store={store}>
<App />
</Provider>
, appDiv)
// in App.js
// This looks the same
// in AccountRouter.js
// This now has Route protection
const AccountRouter = () => {
const accountData = useSelector((state) => state.account);
return (
<div className={styles.container}>
{!accountData.isLoggedIn ? <Redirect to='/log-in' /> : null}
<SideBar />
<Switch>
<Route path='/account/settings' component={AccountSettingsPage} />
<Route path='/account/profile' component={ProfileSettingsPage} />
<Route exact path='/account' component={ProfileSettingsPage} />
</Switch>
</div>
);
};
// in SideBar.js
// This looks the same.
Before I added Redux, the Sidebar was properly redirecting.
After Redux, I have the following behaviour:
When the SideBar is outside of the Switch, you can properly navigate to the Help page, but the components don't render, when I try to navigate to the pages inside the AccountRouter Switch. When I move the SideBar into the Switch, the links to the pages inside this switch start working again, but the /help link stops working.
Is there a way of having links to both inside and outside of this Switch in the same SideBar? How could Redux have affected the Router?

Creating 2 UIs with different headers using router react but subpages cannot open [duplicate]

I need to create 2 multi-pages UIs(managementUI and documentUI) with different headers.
updates:
the header contains navigation buttons to open and display different subpage components between the header and footer of the UI it belongs to.
e.g. the ApiCardGrid component will be displayed in managementUI.
However, the subpage cannot open in the outlet between header and footer when I clicked button in navigation header.
in App.tsx
function App() {
const [totalApiData, setTotalApiData] = useState([]);
useEffect(() => {
axios.get("/api-documents").then((res) => {
setTotalApiData(res.data);
});
}, []);
return (
<>
<div className="App">
<Router>
<Routes>
<Route
path="/apiManagement"
element={<ManagementUI />}
>
<Route
path="apis"
element={<ApiCardGrid spacing={2} size={3} />}
/>
</Route>
<Route
path="/documents"
element={<DocumentUI />}
></Route>
</Routes>
</Router>
</div>
</>
);
}
(update) Following comments made by #Drew Reese, in ManagementUI, I put an Outlet between header and footer to render the contents of subpages like ApiCardGrid. In ManagementUI.tsx:
function ManagementUI() {
const [totalApiData, setTotalApiData] = useState([]);
useEffect(() => {
axios.get("/api-documents").then((res) => {
setTotalApiData(res.data);
});
}, []);
return (
<>
<div className="management-ui">
<div className="management-header">
<Header />
</div>
<div className="management-content">
<Outlet />
</div>
<div className="management-footer">
<Footer />
</div>
</div>
</>
);
}
(update)API List button that link to /apis to display ApiCardGrid component in the Header:
<Header>
<Toolbar
disableGutters
variant="dense"
id="header-primary-navigation"
className="gds-primary-navigation"
>
<nav>
<Button className="gds-button-primary-navigation" href="/apiManagement/apis">
API List
</Button>
<Link to="/apiManagement/apis">API List</Link>
<Button className="gds-button-primary-navigation" href="/apiInfo">
API Info
</Button>
<Button className="gds-button-primary-navigation" href="/addApis">
Add API
</Button>
<Button
className="gds-button-primary-navigation active"
href="/active"
>
active page
</Button>
</nav>
</Toolbar>
</ Header>
similar in Header2
However, when I open UI1 localhost:3000/UI1, the UI1 opened successfully, but if I click button subpage1 to try to display subpage1 in UI1, the console responds error:"No routes matched location "/subpage1".(solved following answer of #Drew Reese)
update:
When I input url http://localhost:3000/apiManagement, the UI shows up. However, when I clicked the API List button, the url jumps to http://localhost:3000/apiManagement/apis, but the subpage item not shows up. I opened inspect tool, but no errors in console.
The subpage(ApiGridCard) supposed to display like
When rendering components on routes that render descendent routes the parent routes necessarily need to append the path wildcard "*" to their paths to allow descendent path matching.
Example:
<Router>
<Routes>
<Route
path="/UI1/*"
element={<UI1 />}
/>
<Route
path="/UI2/*"
element={<UI2 />}
/>
</Routes>
</Router>
An alternative is to create UI layout routes that render the appropriate header component and an Outlet for nested routes to render their element into.
Example:
import { Outlet } from 'react-router-dom';
const UI1 = () => (
<div >
<div >
<Header1 />
</div>
<div >
<Outlet />
</div>
<div >
<Footer />
</div>
</div>
);
const UI2 = () => (
<div >
<div >
<Header2 />
</div>
<div >
<Outlet />
</div>
<div >
<Footer />
</div>
</div>
);
...
<Router>
<Routes>
<Route path="/UI1" element={<UI1 />}>
<Route path="subpage1" element={<Subpage1 />} /> // "/UI1/subpage1"
<Route path="subpage2" element={<Subpage2 />} /> // "/UI1/subpage2"
</Route>
<Route path="/UI2" element={<UI2 />}>
<Route path="subpage3" element={<Subpage3 />} /> // "/UI2/subpage3"
<Route path="subpage4" element={<Subpage4 />} /> // "/UI2/subpage4"
</Route>
</Routes>
</Router>

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>

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>
);
}

Getting Route Location outside of the Route

I am trying add a custom styling to the active route inside the navigation component inside the dashboardlayout, but I am not able to get the current route inside the current component.
const Index = props => {
return (
<BrowserRouter>
<DashboardLayout>
<Route exact path='/' component={Dashboard} />
<Route exact path='/earnings' component={Earnings} />
<Route exact path='/comms' component={Comms} />
<Route exact path='/rankings' component={Rankings} />
<Route exact path='/ratings' component={Ratings} />
<Route exact path='/ads' component={Ads} />
<Route exact path='/settings' component={Settings} />
<Route exact path='/ad/details' component={AdDetails} />
<Route exact path='/ad/submit-sample' component={SubmitSample} />
<Route exact path='/feedback' component={Feedback} />
</DashboardLayout>
</BrowserRouter>
);
};
export default Index;
I made a little component that looks like this:
import React from 'react';
import { Route, Link } from 'react-router-dom';
const Nav = ({
children,
to,
exact,
onClick,
className,
...rest
}) => (
<Route
path={to}
exact={exact}
children={({ match }) => (
<Link
// Assign class and active class name
className={match ? `${className} act-link` : className}
to={to}
replace={match && to === match.path}
onClick={(e) => {
// Avoid clicking the current route
if (!match || to !== match.path) {
onClick(e);
}
}}
{...rest}
>
{children}
</Link>
)}
/>
);
You can use it this way:
const NavList = ({
links,
toggleOpened,
}) => (
<ul className="main-menu">
{
links.map((link) => (
<li
key={link.name}
className={link.className}
>
<NavLink
className="icon-w"
to={link.url}
onClick={(e) => {
e.target.focus();
toggleOpened(false);
}}
>
<div className={`os-icon ${link.icon}`} />
</NavLink>
</li>
))
}
</ul>
);
Hope it helps

Resources