React Switch always going to first Route - reactjs

Here if I am hitting "/" or "/reset_password" or any other route always first component in PurelyUnauthenticatedRoutes gets called i.e. PublicRoute with message ROUTE_CONSTANTS.RESET_PASSWORD
<Router>
<Switch>
<PurelyUnauthenticatedRoutes />
<PublicRoute key={1} redirect={false} path="/" component={CheckAuthenticatedRoutes} />
</Switch>
</Router>
const PurelyUnauthenticatedRoutes = (props) => {
return [
<PublicRoute
{...props}
key={"/reset_password"}
redirect={false}
path={"/reset_password"}
render={(routeProps) => <div>{ROUTE_CONSTANTS.RESET_PASSWORD}</div>}
/>,
];
};
const PublicRoute = (props) => {
const { userAuthenticationDetails, redirect } = props;
const isLoggedIn = (userAuthenticationDetails && userAuthenticationDetails.isLoggedIn) || false;
if (isLoggedIn && redirect) {
return <Redirect to={"/dashboard"} />;
}
return <Route {...props} />;
};
export default PublicRoute;

It's the return statement on your PurelyUnauthenticatedRoutes component that's doing this. You are returning an array of PublicRoute components. React does not know how to render this properly. Instead, you should return multiple routes as children of a Switch component.
It's fine to have multiple Switch statements in a Router. All Route components need to be direct children of a Switch (unless you want to render multiple Routes).
You do need to figure some things out regarding your match conditions because the Switch at the top-level of the the Router means that traffic will go to either PurelyUnauthenticatedRoutes OR PublicRoute - not both. Currently all traffic gets handled by PurelyUnauthenticatedRoutes so that PublicRoute component with key={1} is never shown.
const PurelyUnauthenticatedRoutes = (props) => {
return (
<Switch>
<PublicRoute
{...props}
key={"/reset_password"}
redirect={false}
path={"/reset_password"}
render={(routeProps) => <div>{ROUTE_CONSTANTS.RESET_PASSWORD}</div>}
/>
</Switch>
);
};
Note: you aren't currently calling PurelyUnauthenticatedRoutes with any props when you use it in the App so you don't actually need to accept or pass down that props object if you don't want to.

Related

Using useParams() in the top level

I'm trying to use useParams() in my App component because I need it in two different child components.
But useParams() returns *: "tenants/rdyTupPulEab6mztoLvnQ/projects/0/workspace" not actually able to destructure the tenantId.
I assume this is because App isn't rerendering when the url changes, but I don't want to put two useParams() in both children and send the data back up to app. This is the best place for it to go, but not sure how to get useParams() to destructure the data correctly.
How can I do this, or what alternatives are there?
MRE:
function App() {
console.log(useParams())
useEffect(() => {
(api call that needs the tenantId from useParams())
})
return (
<Routes>
<Route path="/tenants/:tenantId/workspace"
element={<Workspace/>}/>
<Route path="/tenants/:tenantId/setup" element=.
{<Setup/>}/>
</Routes>
)
}
The console.log returns {*: 'tenants/rdyTupPulEab6mztoLvnQ/projects/0/workspace'}.
I need it to return {*: 'tenants/rdyTupPulEab6mztoLvnQ/projects/0/workspace', tenantId: 'rdyTupPulEab6mztoLvnQ'}
The App component can't access the route path params of any of the routes the Routes component is managing. The options you have are to use the matchPath utility to find a "match" to extract the tenentId parameter value.
Something like:
function App() {
const match = useMatch("/tenants/:tenantId/*");
useEffect(() => {
if (match) {
const { tenantId } = match.params;
// ... business logic using tenentId
}
}, [match]);
return (
<Routes>
<Route path="/tenants/:tenantId/workspace" element={<Workspace />} />
<Route path="/tenants/:tenantId/setup" element={<Setup />} />
</Routes>
);
}
An alternative is to create an intermediate layout component that can use the useParams hook.
Example:
import { Outlet, useParams } from 'react-router-dom';
export const TenantIdLayout = () => {
const { tenantId } = useParams();
useEffect(() => {
if (tenantId) {
// ... business logic using tenentId
}
}, [tenantId]);
return <Outlet />;
};
function App() {
return (
<Routes>
<Route element={<TenantIdLayout />}>
{/* "tenantId" routes */}
<Route path="/tenants/:tenantId/workspace" element={<Workspace />} />
<Route path="/tenants/:tenantId/setup" element={<Setup />} />
</Route>
{/* non-"tenantId" routes */}
</Routes>
);
}

You should call navigate() in a React.useEffect(), not when your component is first rendered. (React+ReactROuterDom v6)

So this might be hard to get at first but I'll try to explain everything possible. I'm rendering an App component which uses useNavigation hook from react-router-dom library. Inside AppRoutes I check, if I have $stateParams.redirect and also other values like param1 and param2.I get $stateParams from another custom hook defined in my app. While running the app, I get the log, should navigate now but it actually doesn't navigate to decider-route page instead it stays at / which is <Home /> component. Also I have this warning in console You should call navigate() in a React.useEffect(), not when your component is first rendered. I was wondering why doesn't the navigation takes place to decider-route and the warning is the reason why navigation does not take place?
const App = () => {
return (
<MemoryRouter>
<AppRoutes />
</MemoryRouter>
)
}
const AppRoutes = () => {
const navigate = useNavigate() // react-router-dom v6
if ($stateParams.redirect) {
if ($stateParams.param1 && $stateParams.param2) {
console.log('StateParams : ', $stateParams)
console.log('Should navigate now!')
navigate(`/decider-route/${$stateParams.param1}/${$stateParams.param2}`)
}
}
return (
<Routes>
<Route path="/" element={<Home />} />
<Route
path="/decider-route/:param1/:param2"
element={<Component />}
/>
</Routes>
)
}
The error is preety much self-explanatory. You just need to wrap the navigate() in a useEffect() hook so that it gets executed after the component mounts.
But, in this case, it is being called as soon as the component is first rendered.
navigate() should be triggered by a user action or an useEffect hook in this case. But you're not playing by the rules :)
app.js
const App = () => {
return (
<MemoryRouter>
<AppRoutes />
</MemoryRouter>
);
};
const AppRoutes = () => {
const navigate = useNavigate(); // react-router-dom v6
useEffect(() => {
if ($stateParams.redirect) {
if ($stateParams.param1 && $stateParams.param2) {
console.log("StateParams : ", $stateParams);
console.log("Should navigate now!");
navigate(
`/decider-route/${$stateParams.param1}/${$stateParams.param2}`
);
}
}
}, []);
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/decider-route/:param1/:param2 " element={<Component />} />
</Routes>
);
};

How to use Protected Routes with react-router-dom V6 and typescript?

I am trying to implement a protected route to only allow logged in users to access my app. The code I have written seems to work, I am redirected to my login page when I try to access the homepage without being logged in, however once I am logged in I can access the page but I does not render and I get the following error: Click here for error
I have tried multiple methods and wrapping the element in my protected route seems like the V6 way of doing things, however it doesn't seem to work for me:
My protected route
interface PrivateRouteProps extends RouteProps {
}
const PrivateRoute: React.FC<PrivateRouteProps> = ({...rest}) => {
const auth = useAuth();
if (auth?.user === null) {
return <Navigate to='/'/>;
}
return <Route {...rest}/>;
};
export default PrivateRoute;
My app (route usage)
function App() {
useEffect(() => {
API
.get('api', '/reservation', {})
.then(response => {
console.log(response);
})
.catch(error => {
console.log(error.response);
});
}, [])
return (
<Router>
<Routes>
<Route path='/' element={<LoginPage />}/>
<Route path='/consultAndReserve' element={<PrivateRoute><Navbar/><ConsultReserve/></PrivateRoute>} />
<Route path='/consultAndReserve/:date' element={<><Navbar/><ConsultReserveWithDate/></>}/>
<Route path='/myReservations' element={<><Navbar/><Reservations/></>}/>
<Route path='/tracing' element={<><Navbar/><Tracing/></>}/>
</Routes>
</Router>
);
}
What am I doing wrong?
It's fairly trivial to create a "PrivateRoute" component, even in TypeScript.
In your case you can't directly render a Route component as the error points out. This was a breaking change between RRDv5 and RRDv6. You can render the children prop since you are directly wrapping the components you want to protect.
Example:
const PrivateWrapper = ({ children }: { children: JSX.Element }) => {
const auth = useAuth();
return auth?.user ? children : <Navigate to="/" replace />;
};
Usage:
<Routes>
...
<Route
path="/consoleAndReserve"
element={(
<PrivateWrapper>
<Navbar />
<ConsultReserve />
</PrivateWrapper>
)}
/>
...
</Routes>

Set state before routing to other path

I am using react router to navigate between different pages. Everytime I navigate to another page the state must be updated based on the new path. I use a context to provide this state for all components. My code looks like this:
export const App = () => {
return (
<MyContextProvider>
<Router>
<Switch>
<Route path="/path1" render={(props) => {
const mode = props.match.path.replace('/', '')
return <DataImportWrapper mode={mode} />
}
}/>
</Switch>
</Router>
</MyContextProvider>
)
}
const DataImportWrapper = ({mode}) => {
const { setMode } = useContext(MyContext)
setMode(mode)
return <DataImport />
}
It is very important that the state is updated BEFORE rendering any components of another route because all subcomponents are using that state. With this code I get the warning: Cannot update a component ('MyContextProvider') while rendering a different component ('DataImportWrapper'). and also my state is not updated. Does anyone know what I am doing wrong?
What I would suggest is to set the mode in the Contextprovider and move the Contextprovider within the router. This way you don't need to create an extra wrapper and the mode is always set before rendering other components. You can the set the mode in your ContextProvider
const MyContextProvider = ({ children }) => {
const { path } = useRouteMatch("/path1");
return (
<MyContext.Provider value={{ mode: path.replace('/', '') }}>
{children}
</MyContext.Provider>
)
}
export const App = () => {
return (
<Router>
<Switch>
<MyContextProvider>
<Route path="/path1" ><DataImport /></Route>
</MyContextProvider>
</Switch>
</Router>
)
}

Private route with dynamic URL in react

I have built a PrivateRoute component in React that takes a given component and renders it if the user is logged in or not.
export default function PrivateRoute({component: Component,...rest}) {
return (
<SessionContext.Consumer > {
(context) => {
const {isLoggedIn} = context
return (
<Route {...rest}
render = {
props =>
isLoggedIn ? (
<Component {...props} />
) : (
<Redirect to = "/login" / >
)
}
/>
)
}
}
</SessionContext.Consumer>
);
}
Now in App.js I pass my private component like this:
<PrivateRoute exact path="/dashboard" component={Dashboard} />
Now the issue is when I want to pass a param in the url. This is how I do for the Route component:
<Route path="/user/:user_id" render={(props) =>
<UserComponent user={props.match.params.user_id} />
} />
This syntax unfortunately doesn't work when using my PrivateRouter.
I've tried several other approaches but I can't find a way to get the user_id in my UserComponent.
Am I missing something?
Your PrivateRoute uses prop component. But you pass component through render. if I understand your question correctly.

Resources