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>
Related
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?
I just encountered a problem with the react-router element:
import Home from '../../views/newssandbox/home/Home'
import RightList from '../../views/newssandbox/right-manage/RightList'
import RoleList from '../../views/newssandbox/right-manage/RoleList'
.....
const LocalRouterMap = {
"/home": Home,
"/user-manage/list": UserList,
"/right-manage/role/list": RoleList,
"/right-manage/right/list": RightList,
...
}
I want my code to work as this
<Route path='/home' element={<Home />} />
<Route path='/user-manage/list' element={<UserList />} />
<Route path='/right-manage/role/list' element={<RoleList />} />
How should I change this one?
backRouterList.map(item => {
{/* console.log(item.key) */}
<Route path={item.key} key={item.key} element={LocalRouterMap[item.key]} />
})
Given an array of route path-component pairs:
const LocalRouterMap = {
"/home": Home,
"/user-manage/list": UserList,
"/right-manage/role/list": RoleList,
"/right-manage/right/list": RightList,
...
}
When mapping the array you will need to first create a valid local React component variable and render it out on the Route component's element prop as JSX.
Example:
backRouterList.map(item => {
const Component = LocalRouterMap[item.key];
return (
<Route path={item.key} key={item.key} element={<Component />} />
);
})
You could optimize this a bit by moving the JSX part to the LocalRouterMap object.
const LocalRouterMap = {
"/home": <Home />,
"/user-manage/list": <UserList />,
"/right-manage/role/list": <RoleList />,
"/right-manage/right/list": <RightList />,
...
}
Then you can just pass the element.
backRouterList.map(item => (
<Route
path={item.key}
key={item.key}
element={LocalRouterMap[item.key]}
/>
))
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>
);
}
I've been tearing my hair out all day trying to get this to work. What I'm noticing is that for whatever reason, the transition classes (classNames="fade") never get applied to the page elements. So when I navigate from one page to another, for a brief period (the timeout period) both of the page components will be displayed.
What I should have for 600ms...
<div class="RTG">
<div class="page fade-appear fade-enter fade-enter-active"> <!--
destination component HTML --></div>
<div class="page fade-exit fade-exit-active"> <!-- start component HTML --></div>
</div>
What I get is..
<div class="RTG">
<!-- "fade..." classes never applied to the child nodes" -->
<div class="page"> <!-- destination component HTML --></div>
<div class="page"> <!-- start component HTML --></div>
</div>
And then after the 600ms timeout, I'm left with...
<div class="RTG">
<div class="page"> <!-- destination component HTML --></div>
</div>
NOTE 1: I put the "RTG" className on the TransitionGroup component simply to verify that my "page" class components are actually direct descendants of the TransitionGroup component. Doesn't exist for any other reason.
NOTE 2: I'm using react-transition-group#2.4.0 because I have a compatibility issue with the latest version.
AppRouter.js
import PrivateRoute from './PrivateRoute';
import PublicRoute from './PublicRoute';
import { CSSTransition, TransitionGroup } from 'react-transition-group'
const AppRouter = () => (
<Router history={history}>
<Route render={({location}) => {
return (
<TransitionGroup className="RTG">
<CSSTransition
key={location.key}
timeout={600}
classNames="fade"
>
<Switch location={location}>
<PublicRoute path="/" component={LoginPage} exact={true} />
<PrivateRoute path="/dashboard" component={ExpenseDashboardPage} />
<PrivateRoute path="/create" component={AddExpensePage} />
<PrivateRoute path="/edit/:id" component={EditExpensePage} />
<Route component={NotFoundPage} />
</Switch>
</CSSTransition>
</TransitionGroup>
);
}} />
</Router>
);
export default AppRouter;
PrivateRoute.js
export const PrivateRoute = ({
isAuthenticated,
component: Component,
...rest
}) => (
<Route {...rest} component={(props) => (
isAuthenticated ? (
<div className="page">
<Header />
<Component {...props} />
</div>
) : (
<Redirect to="/" />
)
)} />
);
const mapStateToProps = (state) => ({
isAuthenticated: !!state.auth.uid
});
export default connect(mapStateToProps)(PrivateRoute);
PublicRoute.js
export const PublicRoute = ({
isAuthenticated,
component: Component,
...rest
}) => (
<Route {...rest} component={(props) => (
isAuthenticated ? (
<Redirect to="/dashboard" />
) : (
<div class="page">
<Component {...props} />
</div>
)
)} />
);
const mapStateToProps = (state) => ({
isAuthenticated: !!state.auth.uid
});
export default connect(mapStateToProps)(PublicRoute);
Applicable CSS Styles
.page {
position: absolute;
top: 0;
left: 0;
right: 0;
}
.fade-appear,
.fade-enter {
opacity: 0;
z-index: 1;
}
.fade-appear-active,
.fade-enter.fade-enter-active {
opacity: 1.0;
transition: opacity 300ms linear 150ms;
}
.fade-exit {
opacity: 1.0;
}
.fade-exit.fade-exit-active {
opacity: 0;
transition: opacity 150ms linear;
}
Apparently you will need to wrap <Switch> inside a parent component.
CSSTransition works as it will try to inject classNames to its children as props. Which means the child element will need to take these props and pass on as classNames. Thus you will need to wrap <Switch> into another component to make sure this mechanism works properly.
Edit on CodeSandbox
const AppRouter = () => (
<Router>
<Route
render={({ location }) => {
return (
<TransitionGroup className="RTG">
<CSSTransition key={location.key} timeout={600} classNames="fade">
<div>
<Switch location={location}>
<PublicRoute path="/" component={LoginPage} exact={true} />
<PrivateRoute
path="/dashboard"
component={ExpenseDashboardPage}
/>
<PrivateRoute path="/create" component={AddExpensePage} />
<PrivateRoute path="/edit/:id" component={EditExpensePage} />
<Route component={NotFoundPage} />
</Switch>
</div>
</CSSTransition>
</TransitionGroup>
);
}}
/>
</Router>
);
Making fluid beautiful full page transitions can be tricky.
Most solutions implement it on the route component.
Link Approach
Define the animations on a custom Link component. When user navigate, the next page will enter, and the current one will leave.
<Link
to="/some-path"
transition="glide-right"
/>
Of course, for that you must create your own link...
So you can use packages that does that, like react-tiger-transition.
Demo
(I'm the author)
I'm trying to implement React Router v4 in my create-react-app app and my routes work except for the route with an /:id parameter as it just renders a blank page. I've searched for 2 days and tried implementing the solutions here which says to add <base href="/" /> to the head section of index.html and I've also tried following this Medium guide for simple routing but it still does not work. I don't understand, what is going on?
My code is below, any help is greatly appreciated!
Index.js
ReactDOM.render((
<BrowserRouter>
<App />
</BrowserRouter>),
document.getElementById('root'));
registerServiceWorker();
App.js
class App extends Component {
render() {
return (
<Routes />
)
}
}
Routes.js
export const Routes = () => {
return (
<main>
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/movies" component={Search} />
</Switch>
</main>
);
}
Search.js
render() {
let filteredMovies = this.state.movies.filter((movie) => {
return movie.title.toLowerCase().indexOf(this.state.search.toLowerCase()) !== -1;
})
return (
<div>
<p>Search Page</p>
<form>
<input type="text" value={this.state.search} onChange={this.updateSearch}/>
</form>
<div>
{filteredMovies.map((movie, idx) =>
<div>
<div key={idx}>
<Link to={`/movies/${movie.videoId}`}>
<img src={movie.image.high.url} height="160px" width="100px" alt=""/>
<p>{movie.title}</p>
<p>{movie.quality}</p>
</Link>
</div>
</div>
)}
</div>
<Switch>
<Route path="/movies/:id" component={Single} />
</Switch>
</div>
)
}
}
export default Search;
Single.js
class Single extends Component {
render() {
return (
<div>
<p>Single Movie...</p>
{this.props.match.params.id}
</div>
)
}
}
export default withRouter(Single);
Every route works except for /movies/:id where it just renders a completely blank page. It doesn't even show the <p>Single Movie...</p>.
You should remove exact from route that corresponds to Search component in your Routes, i. e.:
export const Routes = () => {
return (
<main>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/movies" component={Search} />
</Switch>
</main>
);
}
The explanation is pretty simple: the exact prop means that your component will render only if current route is exactly the same as you specified in the path prop
Update
If you want to render the list of movies only when no :id is specified, you should render your list in Switch:
<Switch>
<Route exact path="/movies" render={props => {
return (
<div>
{filteredMovies.map((movie, idx) =>
<div>
<div key={idx}>
<Link to={`/movies/${movie.videoId}`}>
<img src={movie.image.high.url} height="160px" width="100px" alt=""/>
<p>{movie.title}</p>
<p>{movie.quality}</p>
</Link>
</div>
</div>
)}
</div>
)
} />
<Route path="/movies/:id" component={Single} />
</Switch>