I'm practicing nested routing using react-router-dom.
In Room001Container, there is a tab navigation bar that will be displayed on all components, and there is a button that navigates to a detailed post page when the button is pressed.
When I enter the room, I should see the post page first by default.
However, if I output postId as useParams in Room001Container component, undefined is returned.
In the PostDetail component, params are printed well, but why is it not output in the Room001Container? I know that the container is unconditionally rendered even if I move to each component, but it's really strange. Is there any way to know the params from the top container?
function Container() {
return (
<Switch>
<Route
path="/channel/:channelId/lobby/:lobbyId"
component={LobbyContainer}
/>
</Switch>
);
}
function LobbyContainer() {
return (
<Switch>
<Route
exact
path="/channel/:channelId/lobby/001"
component={Room001Container}
/>
<Route
exact
path="/channel/:channelId/lobby/002"
component={Room002Container}
/>
<Route
exact
path="/channel/:channelId/lobby/002"
component={Room003Container}
/>
</Switch>
);
}
function Room001Container() {
const history = useHistory();
const { pathname } = history.location;
const { channelId, postId } = useParams();
console.log(postId) // undefined
return (
<>
<S_TabBar>
<Link to="/channel/:channelId/lobby/001/post">
<S_TabCell>post</S_TabCell>
</Link>
<Link to="/channel/:channelId/lobby/001/chat">
<S_TabCell>chat</S_TabCell>
</Link>
</S_TabBar>
<S_Button onClick={() => history.push('/channel/:channelId/lobby/001/post/1')}>
go to detail
</S_Button>
<Switch>
<Route
exact
path="/channel/:channelId/lobby/001/post"
component={Post}
/>
<Route
exact
path="/channel/:channelId/lobby/001/post/:postId"
component={PostDetail}
/>
<Route
exact
path="/channel/:channelId/lobby/001/chat"
component={Chat}
/>
<Redirect
exact
from="/channel/:channelId/lobby/001"
to="/channel/:channelId/lobby/001/post"
/>
</Switch>
</>
);
}
There is no postId parameter in the route that mounts Room001Container.
You'll want e. g.
<Route
exact
path="/channel/:channelId/lobby/:postId"
component={Room001Container}
/>
Related
So recently I found out two ways of creating private routes in react.
With a HOC (higher-order component):
const PrivateRoute = ({ user, children }) => {
if (!user) {
return <Navigate to="/home" replace />;
}
return children;
};
const App = () => {
...
return (
<>
...
<Routes>
<Route path="/home" element={<Home />} />
<Route
path="/privateroute"
element={
<PrivateRoute user={user}>
<PrivateComponent />
</PrivateRoute >
}
/>
...
</Routes>
</>
);
};
With substituting routes completely
const App = () => {
...
return (
<>
{user ? (
<Routes>
<Route path="/home" element={<Home />} />
<Route path="/privateroute" element={<PrivateComponent />} />
...
</Routes>
) : (
<Routes>
<Route path="/home" element={<Home />} />
...
</Routes>
)}
</>
);
}
My fellow colleague told me that the second way is quite bad since it completely erases some routes (if user is falsy then there is no route to /privateroute). But on my question why might that be bad he had no definitive answer. I couldn't find anything on the internet either. Any thoughts on which way is the best?
Between these two options, the first is the preferred solution since it keeps all routes mounted so they there will be no race condition between setting the user state and issuing an imperative navigation action to one of the protected routes. In other words, with the second implementation you have to wait for the user state to update and trigger a component rerender so the protected routes are mounted and available to be navigated to.
The second method also duplicates unauthenticated routes if it's all one or the other. Code duplication should be avoided.
Note however though that the first example isn't a Higher Order Component, it's just a wrapper component.
Note also that it's more common to create a PrivateRoute component as a Layout Route instead of as a Wrapper component. The change is trivial but it makes the component a little more wieldy. Render an Outlet component for nested routes instead of the children prop for a single wrapped child component.
import { ..., Outlet } from 'react-router-dom';
const PrivateRoute = ({ user }) => {
return user ? <Outlet /> : <Navigate to="/home" replace />;
};
Now instead of wrapping each individual route you want to protect you render a layout route that wraps an entire group of routes you want to protect. It makes your code more DRY.
const App = () => {
...
return (
<>
...
<Routes>
<Route path="/home" element={<Home />} />
... other unprotected routes ...
<Route element={<PrivateRoute />}>
<Route path="/privateroute" element={<PrivateComponent />} />
... other protected routes ...
</Route>
... other unprotected routes ...
</Routes>
</>
);
};
Target
I'm using react router v6.
I have a parent route with a static header with a back button that when I click it needs to go one path above, exp. /shop/1/item/create to /shop/1/item, when i click the back button i call the function navigate from useNavigate()
example
Red is the root page, yellow is where the static header is and green is content that i want to change
Problem
When i call navigate regardless of using "." or ".." or "./" the page never change correctly, it either redirects from /shop/1/items/create to /shop or to /shop/1
the only way of redirecting correctly is using -1 but it causes the problem that if someone copy's the URL when going back they get redirected to whatever page they were before pasting the URL.
Code
const ShopPage = () => {
const [ state, setState ] = React.useState<IShopPageState>({ shop: { name: "" }, isInvalid: false })
const { id } = useParams()
const navigate = useNavigate()
return (
<div id="shop-page">
<Card>
<div id="shop-page-header">
<Icon canHover onClick={() => navigate("./")} icon="BiArrowBack"/>
<Title text={state.shop ? state.shop.name : ""}/>
</div>
</Card>
<div id="shop-page-content">
<Routes>
<Route path="/*" element={<div onClick={() => navigate('./items')}>wddw</div>}/>
<Route path="/items/*" element={<ItemsPage shop={state.shop}/>}/>
<Route path="/items/create" element={<ItemPage/>}/>
</Routes>
</div>
</div>
)
}
Here is the code of the yellow part with only the important stuff, the header is static and always visible and where the back button is.
I believe this code is enough to understand, the green part only redirects the url from /items/ to /items/create using navigate('./create')
I think a possible solution would be simply copying pasting the header for each page but i find that to be bad practice and it is a last resort solution
Here is a example of the problem
EDIT
As asked here is some extra code showing the problem
App.js
export default function App() {
return (
<BrowserRouter>
<Routes>
<Route path="*" element={<Link to="/shop">Shop</Link>} />
<Route path="shop/*" element={<Shop />} />
</Routes>
</BrowserRouter>
);
}
Shop.js
const Shop = () => {
return (
<>
<div>
<Link to="./">Back</Link>
</div>
<Routes>
<Route path="*" element={<Link to="./items">All</Link>} />
<Route path="items/*" element={<Link to="./create">Item</Link>} />
<Route path="items/create" element={<>Create</>} />
</Routes>
</>
);
};
if you are using a nested route then better to use
<Route path="/" >
<Route index element={<Items/>} />
<Route path="item:id" element={<Item/>} />
</Route>
Hey Dear #pekira you can see just a simple hint in the code below for React Router v6
import { Routes, Route, Link } from 'react-router-dom';
const App = () => {
return (
<>
<h1>React Router</h1>
<nav>
<Link to="/home">Home</Link>
<Link to="/user">User</Link>
</nav>
<Routes>
<Route index element={<Home />} />
<Route path="home" element={<Home />} />
<Route path="user/:Id" element={<User />} />
<Route path="*" element={<NoMatch />} />
</Routes>
</>
);
};
Split my routes into two (for now, going to be four later) separate components.
const RouterComponentOne = () => {
return (
<Switch>
<Route path="/routeA" component={ComponentA} />
<Route path="/routeB" component={ComponentB} />
</Switch>
)
}
const RouterComponentTwo = () => {
return (
<Switch>
<Route path="/routeC" component={ComponentA} />
<Route component={Component404} />
</Switch>
)
}
const RouterDefault = () => {
return (
<>
<RouterComponentOne />
<RouterComponentTwo />
</>
)
}
export default RouterDefault;
When visiting any route from RouterComponentOne the 404 component is rendered (which it shouldn't). It renders on non-existent routes (example /test/123/abc which is correct, and doesn't render on any RouterComponentTwo route.
I added some dummy components to the RouterDefault and the 404 component rendered on all additional paths as well.
In short, the 404 route isn't matched/rendered within the routes of the component where the 404Route is. If put into the RouterComponentOne it won't render on routeA and routeB but will render for routeC
How can I solve this?
You need a property called exact
const RouterComponentOne = () => {
return (
<Switch>
<Route path="/routeA" exact component={ComponentA} />
<Route path="/routeB" exact component={ComponentB} />
</Switch>
)
}
Please see sandbox.
I'm having this code:
<Router history={createBrowserHistory()}>
<Switch>
<Route path="/items/:itemId">
<Item />
</Route>
<Route exact path="/items">
<Items />
</Route>
<Redirect to="/items" />
</Switch>
</Router>
Expecting /items to be default route. Items is loading links for available items:
const { url } = useRouteMatch();
return (
<div>
<h1>Items</h1>
{items.map((item) => (
<Link to={`${url}/${item}`}>{item}</Link>
))}
</div>
);
Once an item link is clicked, I'm expecting the url to have the item in it, and to render Item.
url is /items, so everything should work out for Switch to render:
<Route path="/items/:itemId">
<Item />
</Route>
However, it's staying at /items, and not even rendering Items component anymore. No error is shown. Any idea why this is happening?
For router you are missing an exact props which is really important for your case
<Route exact path="/items/:itemId" />
and inside your Item component you misspelled the param name, because in router it's itemId where the component has a different one item, so fixing it will make the param accessible(they should be exact match)
export const Item = () => {
const { itemId } = useParams();
return (
<div>
<h1>Item</h1>
{itemId}
</div>
);
};
That because when it reaches the /items/:itemId Page the component get redirect back to /items , So the solution to this is just remove the redirect component
<Switch>
<Route path="/items/:itemId">
<Item />
</Route>
<Route exact path="/items">
<Items />
</Route>
</Switch>
Also there few other mistake which i saw in your code.In Item.jsx , your using
const { item } = useParams();
This isn't going to the render the anything since it is undefined , that is because your are passing the /:itemId in Route. So try this instead
const { itemId:item } = useParams();
here we are renaming the itemId to item inorder to accessable down in the code.
I'm trying to do layouts with react-router.
When my user hits / I want to render some layout. When my user hits /login, or /sign_up I want the layout to render, with the relevant component for /login or /sign_up rendered.
Currently, my App.js looks like this
return (
<div className={className}>
<Route path="/" component={Auth} />
<ModalContainer />
</div>
);
My Auth.js looks like this
return (
<AuthFrame footerText={footerText} footerClick={footerClick}>
<Route path="/login" component={LoginContainer} />
<Route path="/sign_up" component={SignUpContainer} />
</AuthFrame>
);
So AuthFrame will get rendered when I hit /, and then react router looks for login or sign_up to render the other containers.
However, when I hit /, only the AuthFrame will render.
I would like for / to be treated as /login.
How do I achieve this?
The Switch component is useful in these cases:
return (
<AuthFrame footerText={footerText} footerClick={footerClick}>
<Switch>
<Route path="/login" component={LoginContainer} />
<Route path="/sign_up" component={SignUpContainer} />
{/* Default route in case none within `Switch` were matched so far */}
<Route component={LoginContainer} />
</Switch>
</AuthFrame>
);
see: https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/Switch.md
I think you're forced to introduce a prop/state which indicates the status of your viewer. This means is he signed in or just a guest of your website.
Your router can't obviously render /login if you you hit / but the router allows you to redirect to another page:
class AuthContainer extends React.Component {
defaultProps = {
loggedIn: false
}
render() {
return <div>
<Route path="/login" component={LoginContainer}/>
<Route path="/sign_up" component={SignUpContainer}/>
</div>
}
}
class PublicHomePage extends React.Component {
render() {
return <div>
<Route path="/settings" component={SettingsComponent}/>
<Route path="/profile" component={ProfileComponent}/>
<Route path="/and_so_on" component={AndSoOnComponent}/>
</div>
}
}
class App
extends React.Component {
defaultProps = {
loggedIn: false
}
render() {
const {loggedIn} = this.props;
if (loggedIn) {
return <PublicHomePage/>
}
return <Route exact path="/" render={() => (
<Redirect to="/login"/>
)}/>
}
}
I hope this code works for you. It isn't quite perfect but it should give you an idea how you could solve your problem.
In your case I would probably manipulate a bit with Routes in react-router. This code in AuthFrame should do the trick:
return (
<AuthFrame footerText={footerText} footerClick={footerClick}>
{["/", "/login"].map((path, ind) =>
<Route exact key={ind} path={path} component={LoginContainer} />
)}
<Route exact path="/sign_up" component={SignUpContainer} />
</AuthFrame>);
Note the usage of exact on the routes, this is to prevent matching login component on /sign_up since it will also match / and prevent rendering both login and signup when accessing the root path (/).