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>
Related
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>
);
}
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>
);
};
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;
How can I pass props to the <Outlet /> component when I am nestin routes?
// Parrent Comp
{
const [format, setFormat] = useState('rgb');
const color = [hex, rgb, rgba]
// ...
return (
<PaletteNavBar changeFormat={setFormat}/>
{colorId ? <ColorBoxes /> : <Outlet color={color[format]} />}
// setFormat(hex) is called down here
<PaletteFooter />;
)
}
I don't want to pass them via URL parameters.
The Outlet doesn't take any props, nor will it pass anything as props on to what it renders. It is simply an output for children routes of a component.
Pass them to the component rendered by the Route, it's the Route that is rendered into the Outlet.
Example:
const Layout = () => (
<div className="super-style">
<h1>My super awesome layout container</h1>
<Outlet /> // <-- children routes rendered here
</div>
);
...
<Routes>
<Route element={<Layout />}>
<Route // <-- rendered into Outlet of Layout
path="..."
element={<Component foo="bar" />} // <-- pass props to component here
/>
... other routes for outlet
</Route>
</Routes>
However, the Outlet does provide a React context that can be accessed via the useOutletContext hook since it's a common enough pattern for parent routes to manage some state or value to be shared with children routes.
const Layout = () => (
const [foo, setFoo] = React.useState();
<div className="super-style">
<h1>My super awesome layout container</h1>
<Outlet context={[foo, setFoo]} /> // <-- pass context value
</div>
);
...
<Routes>
<Route element={<Layout />}>
<Route path="..." element={<Component />} />
... other routes for outlet
</Route>
</Routes>
...
import { useOutletContext } from 'react-router-dom';
const Component = () => {
const [foo, setFoo] = useOutletContext(); // <-- access context value
...
};
you better use outlet context to pass the props from parent to nested routes
https://reactrouter.com/docs/en/v6/api#useoutletcontext
Let's say I have 2 components, app and project
I want to have control of elements in my project component from my app component.
When not using a route I can just do the following in my app component
const App = () => {
const ProjectRef = useRef(null);
return (
<Project ref={projectRef}>
);
}
Then in my project component I can get the reference as such
const Project = ({}, ref) => {}
My issue is how to achieve this when using a route. I have tried the following in my app.js component:
<Switch>
<Route path="/projects/:slug">
<Project ref={projectRef}/>
</Route>
</Switch>
This works in that I can get the ref, however in this method I can't seem to get access to the information about the route, which I need for this dynamic route.
Previously I did the following to get the slug
const Project = ({ match, location }) => {
// Get slug
const {params: { slug }} = match;
}
I tried the following, but it does not work, match is empty.
const Project = ({ match, location }, ref) => {}
I am looking for a way to achieve this ideally with functional components.
You are not passing the router props to the defined router component Project. So, just passing props to the component should do the work in your case.
Just replace this
<Switch>
<Route path="/projects/:slug">
<Project ref={projectRef}/>
</Route>
</Switch>
with this
<Switch>
<Route
path="/projects/:slug"
render={(props) => <Project {...props} ref={projectRef} />}
/>
</Switch>