react-router-dom v6: Route's render prop equivalent in v6 - reactjs

in my routes.js with react router v6 this block of code would work but now it's unable to render the component. Instead i get this error
Matched leaf route at location does not have an element. This means it will render an with a null value by default resulting in an "empty" page.
<Route
path="/callback"
render={(props) => {
context.handleAuth(props);
return <Callback />;
}}
/>
I'm trying to figure out how to pass the props into my callback component in v6

Version 6 favours element over components to. In version 6, you may do it this way:
Use the follwing in <Callback />
const params = useParams();
useEffect(() => {
/* If context is avialable here. */
context.handleAuth(params);
}, [])
For more info, read the doc here:
https://reactrouter.com/docs/en/v6/upgrading/v5#advantages-of-route-element
See more:
https://medium.com/frontendweb/how-to-pass-state-or-data-in-react-router-v6-c366db9ee2f4

Related

useffect inside <outlet> component does not trigger

Parent component:
<Main props... >
<LinksArray />
<Outlet context={investorId}/>
</Main>
outlet component
const NewBoards: React.FC = () => {
let { boardId } = useParams();
// this does not
useEffect(() => {
console.log('ue trigger page change',boardId )
}, []);
// this triggers (because of an argument
useEffect(() => {
console.log('ue trigger page change',boardId )
}, [boardId ]);
return (
<>
{console.log('>rerender')}
<p> newBoards</p>
<p>{boardId}</p>
</>
)
}
NewBoards is an outlet element, I would love for useEffect to trigger on a ROUTE (ex. boards/123 to boards/345 ) change, without passing boardId, however useEffect does not trigger on the address change unless I`m passing boardId to the dependency array. boardId param does change.
Also I would like to know why that useEffect does not trigger. I can't find anything related to it on the react router v6 official documentation
edit:
I have also noticed that states are saved. States inside outlet component( NewBoards ) do not refresh to initial ones.
edit ( router definition ) :
{
path: '/boards/',
element: authorized ? <Boards1 /> : <Login />,
children: [{
path: '/boards/:boardId',
element: authorized ? <NewBoards /> : <Login />,
}]
},
From what I see from your comments you've seriously misunderstood what is happening between the Outlet and the nested Route components that are rendering their content, i.e. element prop, into it.
Assuming authorized is true then the following route config:
{
path: '/boards/',
element: authorized ? <Boards1 /> : <Login />,
children: [{
path: '/boards/:boardId',
element: authorized ? <NewBoards /> : <Login />,
}]
},
will produce the following rendered routes:
<Routes>
...
<Route path="/boards" element={<Boards1 />}>
<Route path="/boards/:boardId" element={<NewBoards />} />
</Route>
...
</Routes>
Where I think your understanding goes awry is in thinking that when the URL path changes from "/boards/123" to "/boards/345" that the Boards1 component rendered on "/boards" will rerender and remount the Outlet. It won't. This means that the Outlet it is rendering also doesn't do anything other than output the result of the currently matched route.
A second point of confusion/misudnerstanding on your part is thinking that when the URL path changes from "/boards/123" to "/boards/345" that <Route path="/boards/:boardId" element={<NewBoards />} /> will unmount and remount a new instance of the NewBoards component. This is also not the case. The NewBoards component will remain mounted and will simply rerender. This is an optimization by react-router-dom as it's a lot more work to tear down and remount a component than it is to simply rerender it with some different props/context/etc.
The routed components using the route path params necessarily need to "listen" to changes to the params if they need to issue side-effects based on the param values. This is why the useEffect hook with empty dependency array runs only once when the NewBoards component was mounted (not each time the route changes) and the useEffect hook with the boardId param as a dependency correctly reruns each time the boardId value changes.
const NewBoards: React.FC = () => {
const { boardId } = useParams();
// Run this when component mounts, occurs once per mounting
useEffect(() => {
console.log('ue trigger page change', boardId);
}, []);
// Run this when the `boardId` param updates
useEffect(() => {
console.log('ue trigger page change', boardId);
}, [boardId]);
// Run this each render, i.e. no dependency array at all!
useEffect(() => {
console.log('>rerender');
});
return (
<>
<p>newBoards</p>
<p>{boardId}</p>
</>
);
};

React router v6 testing library navigate relative fails

In my application i use relative navigation with react router v6 (we are currently migrating) and it works fine.
When i try to do some tests using Jest and testing-library the relative navigation fails and always resolve with the pathname '/'.
I use this wrapper to render my component under test to add it a Router.
const WrapTestComponentInRouter = (TestComponent, initialRoute = '/', history =['/']) => {
function WrappedComponent(props) {
return (
<MemoryRouter initialEntries={history}>
<Routes>
<Route path={initialRoute} element={<TestComponent {...props} />} />
<Route path="*" element={<Location />} />
</Routes>
</MemoryRouter>
);
}
return WrappedComponent;
};
function ComponentToTest() {
const location = useLocation();
console.log(location);
const path = useResolvedPath('..');
console.log(path);
return <button onClick={() => navigate('..')}>navigate</div>;
}
I use the hook useResolvedPath to compute the navigation result and log it. In my application the relative path computed is correct but in my test it is always equals to '/' whatever the current location is.
Does someone has encounter and solved this problem?
EDIT
Ok, i understand what is the problem. The useResolvedPath is using the routingContext to compute the relative route so if we use '..' it pop to the previous route in the route tree.
With that, if i want to test my navigation i need to reproduce my route tree. Another solution is to use the real relative path by using the method resolvePath('..', location)
The resolvePath compute relative path from a path
resolvePath('..', '/user/project') ---> '/user'
the navigate function also use the RoutingContext so if i want to do a relative from the current path i have to do
const location = useLocation();
navigate(resolvePath('..', location.pathname));

#cypress/react : How to launch a component under a given path / URL?

I have a React component 'Foo' that uses react-router to decide which child to mount.
Let's say this component has 3 submodules: A, B, C.
In a 'normal' cypress test I would use cy.visit('www.test.com/foo/subModuleA') and I would get my site and it would display my Foo-component, which would render the module 'A'.
My component test however mounts this component directly, and cy.visit(...) doesn't work.
cy.visit from a component spec is not allowed
see https://github.com/bahmutov/#cypress/react/issues/286
(Sadly this link leads to nowhere)
For the time being I wrote myself a helper-component:
function PathEnforcer (props: React.PropsWithChildren<{ path: string }>) {
const history = useHistory()
useEffect(() => {
history.push(props.path)
}, [])
return <>{props.children}</>
}
This simply switches the path as soon as it mounts.
Now I can do something like this in my test:
mount(
<HashRouter>
<PathEnforcer path='subModuleA'>
<Foo />
</PathEnforcer>
<HashRouter>
)
And my component is displaying the right module.
This is a workaround that I can work with, but it can hardly be a long term best practice.
How can I configure the tests / execute a command that directly mounts this component given the right path?
I was running into the same issue and this question came up. With react-router v6 this works:
mount (
<MemoryRouter initialEntries={['/foo']}>
<Routes>
<Route path={'/:segment'} element={<YourComponent />}>
</Routes>
</MemoryRouter>
)

React router two URL parameters &more

I'm using react-router-dom and this is currently how it looks:
<Switch>
<Route path={"/layouts/:layoutID"} component={Layouts} />
<Route
path={"/dashboard/:dashboardID"}
component={Dashboards}
/>
</Switch>
When the user navigates to "/dashboard/:dashboardID" inside this component he can choose a sub-page onClick, and I want the URL structure will be "/dashboard/:dashboardID/:pageID" pageID will navigate to 'PageIdComponent' this component will get a 'match' props and will show pageID
Please see the attached file that shows the necessary structure.
What is the best way to implement it?
If you have the child route inside your Dashboard component, then you can use the match.path value to build up a sub route:
<Route path={`${props.match.path}/:dashboardId`}>} component={SubDashboard} />
in order to make the path match what the parent's had and then add a new variable on top of that.
Then you can navigate to it using the match.url to make a link to the current URL and add the subPage to it:
<Link to={`${props.match.url}/pageFour`}>Page four</Link>
Instead of prop driling, React Router offers ways to get the relevant props to whatever components you desire via withRouter HOC or various hooks. The hooks were introduced in version 5.1
To get the match via a hook you can use const match = useRouteMatch()
To get the relevant props via the HOC:
const Example = useRouteMatch(function Example({ match, location, history }) {
return <div>{match.url}</div>;
});
For react router v6 (that's upcoming):
https://reacttraining.com/blog/react-router-v6-pre/#nested-routes-and-layouts

React. How to redirect early in a component lifecycle

This seems so simple, but I am new to react and trying different approaches and nothing is working for me. (BTW I am a regular contributor but working on my clients machine and can't remember my so password.)
The version of react router is 4.0, and state is stored using redux.
The scenario is that we are changing the order of routing in our application and would like to redirect any users that have Urls with the old structure to the new Url structure. I have tried the following (also note that I have "scrubbed" the names of the page, function calls and variables):
There is a trigger component for the section I need to direct from, with routing set up like this:
<Route path='page/:pageGuidGuid' component={PageTrigger}> </Route>
In the trigger component, ComponentWillMount makes a request that returns a 404 if the link is from the previous component, although it does redirect to the correct route. I am checking for and triggering the redirect in getInitialState, but the component keeps going through the lifecycle and ComponentWillMount is called, resulting in a 404 and then redirects to the correct page.
const PageTrigger = connectToStores(React.createClass({
getInitialState() {
this.checkForRedirect();
return {};
},
componentWillMount() {
if (this.props.params.guid) {
this.setCaseByGuid(this.props.params.guid);
}
},
checkFrorRedirect() {
/* logic to determine if it should redirect */
browserHistory.push(redirectUrl);
}
}
I have also tried creating a custom route...
<CaseRedirectRoute path='(cases/:caseGuid)' component={CaseTrigger}>
</CaseRedirectRoute>
And in a separate module (based off a login sample)
const CaseRedirectRoute = ({ component: Component, ...rest }) => (
<Route
{...rest} render={props => (
checkForCaseRedirect(...props) ? (
<Redirect to={{
pathname: getCaseUrlRedirectUrl(...props),
state: { from: props.location }
}} />
) : (
<Component {...props} />
)
)} />
);
I have WithRouter imported from react router. When I try to run this I am getting an error inside the react framework:
Uncaught Type Error: Cannot read property 'createRouteFromReactElement' of undefined
at RouteUtils.js:68
at forEachSingleChild (ReactChildren.js:52)
at traverseAllChildrenImpl (traverseAllChildren.js:98)
at traverseAllChildrenImpl (traverseAllChildren.js:114)
at traverseAllChildren (traverseAllChildren.js:186)
at Object.forEachChildren [as forEach] (ReactChildren.js:70)
at createRoutesFromReactChildren (RouteUtils.js:65)
at Function.createRouteFromReactElement (RouteUtils.js:35)
at RouteUtils.js:69
at forEachSingleChild (ReactChildren.js:52)
I've tried redirecting from app.js and page.js, but the PageTrigger is the first component having state set. I either need to figure out how to stop execution after the redirect, or figure out why my custom route keeps blowing up. Any help would be appreciated.
I'm not sure about your setup, but I would implement this redirect as this:
<Route path='oldurl/:p1/:p2' render={
routeProps => (
<Redirect to={'/newUrlHere/'+routeProps.match.params.p1} />
)
} />
So, if the route matches (you may specify exact match if necessary), when the Redirect "renders" it stops rendering and should start the rendering from the new URL. You may build your URL in the Redirect.to property as you wish

Resources