Passing and spreading props in React Router Dom v 6 - reactjs

I have the below working in RRD v 5.3.0 but cant get an alternative version working in v6. Ive tried using 'element' but it doesn't seem to work
<BrowserRouter>
<Route render={props => (
<>
<Header {...props}/>
<Routes/>
<Footer/>
</>
)}/>
</BrowserRouter>

Version 6 no longer supports the render function style. Instead the preferred approach is to use the use hooks (eg, useLocation, useParams) in the child component to access whatever values are needed.
For example, if header was previously expecting to get location and match via its props:
const Header = ({ match, location }) => {
// ...
}
It would now do:
const Header = () => {
const location = useLocation();
const match = useMatch();
// ...
}
If header can't be modified for some reason, then you could make a component that uses these hooks, and then passes the values down to the header as props:
const Example = () => {
const location = useLocation();
const match = useMatch();
return (
<>
<Header location={location} match={match}>
<Routes/>
<Footer/>
<>
)
}
// used like:
<Route>
<Example/>
</Route>
For a guide on updating from version 5 to 6, see this page
For information about why they made this change, see this article

Related

In React, why route can work properly without path

code as below. {...rest} doesn't contain path, the route shouldn't route properly, but in fact it can route to path(/movies/:id). Why could that happen?
<ProtectedRoute path="/movies/:id" component={MovieForm} />
const ProtectedRoute = ({ path, component: Component, render, ...rest }) => {
console.log("rest: ", { ...rest });
return (
<Route
// path={path}
{...rest}
render={(props) => {
console.log(props);
if (!auth.getCutterntUser()) return <Redirect to="/login" />;
return Component ? <Component {...props} /> : render(props);
}}
/>
);
The reason the route still works is because you are specifying a path prop on the PrivateRoute component and this is the component the Switch uses for path matching.
A review of the v5 Switch source code:
/**
* The public API for rendering the first <Route> that matches.
*/
class Switch extends React.Component {
render() {
return (
<RouterContext.Consumer>
{context => {
invariant(context, "You should not use <Switch> outside a <Router>");
const location = this.props.location || context.location;
let element, match;
// We use React.Children.forEach instead of React.Children.toArray().find()
// here because toArray adds keys to all child elements and we do not want
// to trigger an unmount/remount for two <Route>s that render the same
// component at different URLs.
React.Children.forEach(this.props.children, child => {
if (match == null && React.isValidElement(child)) {
element = child;
const path = child.props.path || child.props.from;
match = path
? matchPath(location.pathname, { ...child.props, path })
: context.match;
}
});
return match
? React.cloneElement(element, { location, computedMatch: match })
: null;
}}
</RouterContext.Consumer>
);
}
}
The Switch iterates over its children and checks for a path or from prop path value, and computes a match. For the computed match it clones and returns the matched element.
<ProtectedRoute
path="/movies/:id" // <-- child component path specified here
component={MovieForm}
/>
In fact, even something as simple as a div with a path prop will work, example:
<div path="/movies/:id">TEST</div>
This only explains the matching within the Switch, but now you're wondering why the Route component being rendered by PrivateRoute still works. This is because any Route not directly rendered by a Switch is now inclusively matched and rendered (as if it were only in a Router component). The Route has no path so it matches anything and is rendered.
It should be sort of obvious now that the Route component itself is rather irrelevant. You can simplify the PrivateRoute a bit; conditionally render a Route with all the route props passed through, or the Redirect to login page.
const ProtectedRoute = (props) => {
return auth.getCutterntUser()
? <Route {...props} />
: <Redirect to="/login" />;
};

Can I create 2 components which share a state?

In my React app, I have a layout file, and I want to be able to pass 2 different components into it. One component is to be shown in Area 1, and another is to be shown in Area 2. Both components need to share information with each other.
So, my layout is:
const SplitLayout = (Area1Content, Area2Content) => {
return (
<div className="area1">
<Area1Content />
</div>
<div className="area2">
<Area2Content />
</div>
);
}
export default SplitLayout;
In my App.js I have:
const App = () => (
<Router>
<Switch>
<Route exact path={ROUTES.HOME}
render={(props) =>
<SplitLayout {...props}
Area1Content={HomeContent}
Area2Content={SidebarContent} />}/>
</Switch>
</Router>
);
export default App;
This works fine; I can put HomeContent and SidebarContent into a file and export both of them, and they are shown correctly.
However, I want to be able to pass information from one to the other, so, for instance, SidebarContent has a list of names; when I click on a name in the list, I want that person's details to be shown in HomeContent (so in the HomeContent component I can have a state variable called currentPerson, and when a name is clicked in SidebarContent, the value of currentPerson should be changed).
Is there a way to achieve this?
I have several pages with similar layouts, so what I'm hoping is that I can have, eg, a HomeComponent.js file which has HomeContent and SidebarContent, and then another component called, say, SecondComponent.js which has SecondContent and SecondSidebar, so I can just add a new Route to App, something like:
<Route exact path={ROUTES.SECOND}
render={(props) =>
<SplitLayout {...props}
Area1Content={SecondContent}
Area2Content={SecondSidebar} />}/>
so it will render the same layout but with different components. I know I could lift the state up to the top level, but there could potentially be several different component pairs, each needing to pass info, and I think it would get messy to manage all of them at the App level. Is there a better way?
EDIT: I think what I want to do is something like this:
In App.js my route would be something like:
<Route exact path={ROUTES.HOME}
render={(props) =>
<SplitLayout {...props}
PageContent={WrapperComponent} />}/>
Then in the SplitLayout file I'd have something like:
const SplitLayout = (WrapperComponent) => {
return (
<div className="area1">
<WrapperComponent.Area1Content/>
</div>
<div className="area2">
<WrapperComponent.Area2Content/>
</div>
);
}
And WrapperComponent would be something like:
const WrapperComponent = () => {
const [myStateVariable, setMyState] = useState("xyz")
const Area1Content = () => {
return (<div>{myStateVariable}</div>);
}
const Area2Content = () => {
return (<div onClick={setMyState("abc")}>Something else</div>);
}
}
export default WrapperComponent;
Is there a way to do something like that?
You can put the shared state in the parent component, and pass it down as props.

useParams hook returns undefined in react functional component

The app displays all photos <Photo> in a grid <PhotoGrid>, then once clicked, a function in <Photo> changes URL with history.push, and Router renders <Single> based on URL using useParams hook.
PhotoGrid -> Photo (changes URL onClick) -> Single based on URL (useParams).
I must have messed something up, becouse useParams returns undefined.
Thanks for all ideas in advanced.
App.js
class App extends Component {
render() {
return (
<>
<Switch>
<Route exact path="/" component={PhotoGrid}/>
<Route path="/view/:postId" component={Single}/>
</Switch>
</>
)
}
}
export default App;
Photogrid.js
export default function PhotoGrid() {
const posts = useSelector(selectPosts);
return (
<div>
hi
{/* {console.log(posts)} */}
{posts.map((post, i) => <Photo key={i} i={i} post={post} />)}
</div>
)
}
in Photo I change URL with history.push
const selectPost = () => {
(...)
history.push(`/view/${post.code}`);
};
Single.js
import { useParams } from "react-router-dom";
export default function Single() {
let { id } = useParams();
console.log("id:", id) //returns undefined
return (
<div className="single-photo">
the id is: {id} //renders nothing
</div>
)
}
When using useParams, you have to match the destructure let { postId } = useParams(); to your path "/view/:postId".
Working Single.js
import { useParams } from "react-router-dom";
export default function Single() {
const { postId } = useParams();
console.log("this.context:", postId )
return (
<div className="single-photo">
{/* render something based on postId */}
</div>
)
}
You should use the same destructure as mentioned in your Route path. In this case, you should have written :
let { postID } = useParams();
I will mention two more mistakes which someone could make and face the same problem:
You might use Router component in place of Route component.
You might forget to mention the parameter in the path attribute of the Route component, while you would have mentioned it in the Link to component.
Ensure the component where you call useParams() is really a child from <Route>
Beware of ReactDOM.createPortal
const App = () => {
return (
<>
<Switch>
<Route exact path="/" component={PhotoGrid}/>
<Route path="/view/:postId" component={Single}/>
</Switch>
<ComponentCreateWithPortal /> // Impossible to call it there
</>
)
}
You have to check API that you are using. Sometimes it's called not just id. That's why useParams() do not see it

React prevent remounting components passed from props

When using React with React Router I run in some mounting issues.
This might not even be a problem with React Router itself.
I want to pass some additional data along with the child routes.
This seems to be working, however the changes on the main page trigger grandchildren to be remounted every time the state is changed.
Why is this and why doe this only happen to grandchildren an not just the children ?
Code example:
import React, { useEffect, useState } from 'react';
import { Route, Switch, BrowserRouter as Router, Redirect } from 'react-router-dom';
const MainPage = ({ ChildRoutes }) => {
const [foo, setFoo] = useState(0);
const [data, setData] = useState(0);
const incrementFoo = () => setFoo(prev => prev + 1);
useEffect(() =>{
console.log("mount main")
},[]);
useEffect(() =>{
setData(foo * 2)
},[foo]);
return (
<div>
<h1>Main Page</h1>
<p>data: {data}</p>
<button onClick={incrementFoo}>Increment foo {foo}</button>
<ChildRoutes foo={foo} />
</div>
);
};
const SecondPage = ({ ChildRoutes, foo }) => {
const [bar, setBar] = useState(0);
const incrementBar = () => setBar(prev => prev + 1);
useEffect(() =>{
console.log("mount second")
},[]);
return (
<div>
<h2>Second Page</h2>
<button onClick={incrementBar}>Increment bar</button>
<ChildRoutes foo={foo} bar={bar} />
</div>
);
};
const ThirdPage = ({ foo, bar }) => {
useEffect(() =>{
console.log("mount third")
},[]);
return (
<div>
<h3>Third Page</h3>
<p>foo: {foo}</p>
<p>bar: {bar}</p>
</div>
);
};
const routingConfig = [{
path: '/main',
component: MainPage,
routes: [
{
path: '/main/second',
component: SecondPage,
routes: [
{
path: '/main/second/third',
component: ThirdPage
},
]
}
]
}];
const Routing = ({ routes: passedRoutes, ...rest }) => {
if (!passedRoutes) return null;
return (
<Switch>
{passedRoutes.map(({ routes, component: Component, ...route }) => {
return (
<Route key={route.path} {...route}>
<Component {...rest} ChildRoutes={props => <Routing routes={routes} {...props}/>}/>
</Route>
);
})}
</Switch>
);
};
export const App = () => {
return(
<Router>
<Routing routes={routingConfig}/>
<Route exact path="/">
<Redirect to="/main/second/third" />
</Route>
</Router>
)
};
export default App;
Every individual state change in the MainPage causes ThirdPage to be remounted.
I couldn't create a snippet with StackOverflow because of the React Router. So here is a codesandbox with the exact same code: https://codesandbox.io/s/summer-mountain-unpvr?file=/src/App.js
Expected behavior is for every page to only trigger the mounting once.
I know I can probably fix this by using Redux or React.Context, but for now I would like to know what causes this behavior and if it can be avoided.
==========================
Update:
With React.Context it is working, but I am wondering if this can be done without it?
Working piece:
const ChildRouteContext = React.createContext();
const ChildRoutesWrapper = props => {
return (
<ChildRouteContext.Consumer>
{ routes => <Routing routes={routes} {...props} /> }
</ChildRouteContext.Consumer>
);
}
const Routing = ({ routes: passedRoutes, ...rest }) => {
if (!passedRoutes) return null;
return (
<Switch>
{passedRoutes.map(({ routes, component: Component, ...route }) => {
return (
<Route key={route.path} {...route}>
<ChildRouteContext.Provider value={routes}>
<Component {...rest} ChildRoutes={ChildRoutesWrapper}/>
</ChildRouteContext.Provider>
</Route>
);
})}
</Switch>
);
};
To understand this issue, I think you might need to know the difference between a React component and a React element and how React reconciliation works.
React component is either a class-based or functional component. You could think of it as a function that will accept some props and
eventually return a React element. And you should create a React component only once.
React element on the other hand is an object describing a component instance or DOM node and its desired properties. JSX provide
the syntax for creating a React element by its React component:
<Component someProps={...} />
At a single point of time, your React app is a tree of React elements. This tree is eventually converted to the actual DOM nodes which is displayed to our screen.
Everytime a state changes, React will build another whole new tree. After that, React need to figure a way to efficiently update DOM nodes based on the difference between the new tree and the last tree. This proccess is called Reconciliation. The diffing algorithm for this process is when comparing two root elements, if those two are:
Elements Of Different Types: React will tear down the old tree and build the new tree from scratch // this means re-mount that element (unmount and mount again).
DOM Elements Of The Same Type: React keeps the same underlying DOM node, and only updates the changed attributes.
Component Elements Of The Same Type: React updates the props of the underlying component instance to match the new element // this means keep the instance (React element) and update the props
That's a brief of the theory, let's get into pratice.
I'll make an analogy: React component is a factory and React element is a product of a particular factory. Factory should be created once.
This line of code, ChildRoutes is a factory and you are creating a new factory everytime the parent of the Component re-renders (due to how Javascript function created):
<Component {...rest} ChildRoutes={props => <Routing routes={routes} {...props}/>}/>
Based on the routingConfig, the MainPage created a factory to create the SecondPage. The SecondPage created a factory to create the ThirdPage. In the MainPage, when there's a state update (ex: foo got incremented):
The MainPage re-renders. It use its SecondPage factory to create a SecondPage product. Since its factory didn't change, the created SecondPage product is later diffed based on "Component Elements Of The Same Type" rule.
The SecondPage re-renders (due to foo props changes). Its ThirdPage factory is created again. So the newly created ThirdPage product is different than the previous ThirdPage product and is later diffed based on "Elements Of Different Types". That is what causing the ThirdPage element to be re-mounted.
To fix this issue, I'm using render props as a way to use the "created-once" factory so that its created products is later diffed by "Component Elements Of The Same Type" rule.
<Component
{...rest}
renderChildRoutes={(props) => (<Routing routes={routes} {...props} />)}
/>
Here's the working demo: https://codesandbox.io/s/sad-microservice-k5ny0
Reference:
React Components, Elements, and Instances
Reconciliation
Render Props
The culprit is this line:
<Component {...rest} ChildRoutes={props => <Routing routes={routes} {...props}/>}/>
More specifically, the ChildRoutes prop. On each render, you are feeding it a brand new functional component, because given:
let a = props => <Routing routes={routes} {...props}/>
let b = props => <Routing routes={routes} {...props}/>
a === b would always end up false, as it's 2 distinct function objects. Since you are giving it a new function object (a new functional component) on every render, it has no choice but to remount the component subtree from this Node, because it's a new component every time.
The solution is to create this functional component once, in advance, outside your render method, like so:
const ChildRoutesWrapper = props => <Routing routes={routes} {...props} />
... and then pass this single functional component:
<Component {...rest} ChildRoutes={ChildRoutesWrapper} />
Your components are remounting every time because you're using the component prop.
Quoting from the docs:
When you use component (instead of render or children, below) the router uses React.createElement to create a new React element from the given component. That means if you provide an inline function to the component prop, you would create a new component every render. This results in the existing component unmounting and the new component mounting instead of just updating the existing component. When using an inline function for inline rendering, use the render or the children prop (below).
The solution you probably need in your case is to edit your Routing component to use render instead of children.

Read URL parameters

I have a very basic app and I want to read the request parameter values
http://localhost:3000/submission?issueId=1410&score=3
Page:
const Submission = () => {
console.log(this.props.location); // error
return ();
}
export default Submission;
App
const App = () => (
<Router>
<div className='App'>
<Switch>
<Route path="/" exact component={Dashboard} />
<Route path="/submission" component={Submission} />
<Route path="/test" component={Test} />
</Switch>
</div>
</Router>
);
export default App;
Did you setup correctly react-router-dom with the HOC in your Submission component ?
Example :
import { withRouter } from 'react-router-dom'
const Submission = ({ history, location }) => (
<button
type='button'
onClick={() => { history.push('/new-location') }}
>
Click Me!
</button>
)
export default withRouter(Submission)
If you already did that you can access the params like that :
const queryString = require('query-string');
const parsed = queryString.parse(props.location.search);
You can also use new URLSearchParams if you want something native and it works for your needs
const params = new URLSearchParams(props.location.search);
const foo = params.get('foo'); // bar
Be careful, i noticed that you have a functional component and you try to access the props with this.props. It's only for class component.
When you use a functional component you will need the props as a parameter of the function declaration. Then the props should be used within this function without this.
const Submission = (props) => {
console.log(props.location);
return (
<div />
);
};
The location API provides a search property that allows to get the query parameters of a URL. This can be easily done using the URLSearchParams object. For example, if the url of your page http://localhost:3000/submission?issueId=1410&score=3 the code will look like:
const searchParams = location.search // location object provided by react-router-dom
const params = new URLSearchParams(searchParams)
const score = params.get('score') // 3
const issueId = params.get('issueId') // 1410

Resources