Using useParams() in the top level - reactjs

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

Related

Passing set statehook function into subroute component. Destructuring error

Using react 18.2 with react-router-dom 6.3
I have my routes in this style
<Routes>
<Route path="/" element = {<App/>}>
<Route path="search" element = {<Content/>} />
<Route path="nextComp/:id" element = {<Component/>} />
</Route>
</Routes>
In my app, I have a dynamic nav bar that I want to stick around which could be used to select different generated components (generated by actions in the Content component in /search) There are states in my App component that need to be set by the Content component. I pass information down with something along the lines of:
const App: React.FC = () => {
const [lock, setLock] = useState<Boolean>(false);
const [lotsQuery, setLotsQuery] = useState<lotWaferQueryInput[]>([]);
const navigate = useNavigate();
const onClickHandle(() => {
navigate('/search', {state: {
setLock : setLock,
setLotsQuery: setLotsQuery
}});
}, []);
}
In my Content component, I try accessing the data with :
const {state} : any = useLocation();
const {setLock,setLotsQuery} : any = state;
This results in
Uncaught TypeError: Cannot destructure property 'setLock' of 'state' as it is null. I understand that you can't directly serialize a function. How should I reapproach the way I'm routing data?
As of now, the hierarchy looks like
App
-Nav (child component)
-Content(search subroute)
-Component(nextComp subroute)
Data is entered in Content, and then sent to App (which is the current problem of being able to set function). Data is handled by App and then passed to nav and generates Component(subroutes)
How can I achieve sending data from a subroute component to a parent route then? Any advice appreciated.
The route state needs to be JSON serializable, so sending functions just won't work. I suggest exposing the functions down to nested routes via the Outlet's context and the useOutletContext hook.
Example:
import { Outlet } from 'react-router-dom';
const App: React.FC = () => {
const [lock, setLock] = useState<Boolean>(false);
const [lotsQuery, setLotsQuery] = useState<lotWaferQueryInput[]>([]);
...
return (
...
<Outlet context={{ setLock, setLotsQuery }} />
...
);
};
In nested route's component:
import { useOutletContext } from 'react-router-dom';
...
const { setLock, setLotsQuery } = useOutletContext();
...
<Routes>
<Route path="/" element={<App />}> // <-- provides context value
<Route path="search" element={<Content />} /> // <-- can access context value
<Route path="nextComp/:id" element={<Component />} />
</Route>
</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>

How to clear redux state once the user visit another route?

I have the following problem: I have a general component that contains some data from the redux store and I want to clear this data once the user visits another route.
<Route path="/create/gallery" element={<CreatePage type={ContentType.gallery}/>} />
also I have some more code that saves my entered data to the store
saveGeneralInfo = (field: string, value: string) => {
const data = {};
data[field] = value;
this.props.dispatch(saveGeneralInfo(data));
}
How I can clear the state if the user leave the page or visit any other link? (For example from header)
if(this.state.keycloak) {
if(this.state.authenticated) return (
<div className="App">
<Header/>
<Routes>
<Route path="/" element={<Content />} />
<Route path="/sites"/>
<Route path="/users"/>
<Route path="/create/gallery" element={<CreatePage type={ContentType.gallery}/>}/>
<Route path="/create/article" element={<CreatePage type={ContentType.article} />} />
<Route path="/create/quiz" element={<CreatePage type={ContentType.quiz} />} />
</Routes>
</div>
);
else return (
<div>Can't authorize</div>
)
}
You will have to provide functionality for store clearing that fires on unMount lifecycle event in every route root component.
If you are using functional components:
export const Component = () => {
const dispatch = useDispatch();
useEffect(() => {
return () => {
dispatch(yourActionThatCleansReduxStore())
}
}, [])
//rest of your code
}
In my case I reset parts of my store for every page URL like /info or /user where store looks like
{
user: {
id: ...
},
info: ...
}
You can create a route controller using children component
import { useDispatch } from "react-redux";
import { useLocation } from "react-router-dom";
import { cleanState } from "Your-reducer.js";
function CleanState({ children }) {
const location = useLocation();
const dispatch = useDispatch();
useEffect(() => {
dispatch(cleanState()); // every time the route changes clean the state
// if you don't use redux-toolkit you can use action.payload etc....
},[location.pathname])
return <>{children}</>;
}
export default CleanState;
then you have to wrap the main component
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import YourComponents from "./YourComponents"; // Your components
import CleanState from "./CleanState"; // where did you save it
function App() {
return (
<Router>
<CleanState> // This is the previous component, this will listen the movements of the routes
<Routes>
<Route path="/main" element={<YourComponents />} />
<Route path="*" element={<YourComponents />} />
</Routes>
</CleanState>
</Router>
);
}
export default App;

React Switch always going to first Route

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.

Resources