Accessing a URL parameter outside the element in react-router-v6 - reactjs

Issue: I need to access a URL parameter outside the component in v6.
I know I can use the useParams() hook inside the component, But what if I needed to access a URL parameter outside the component?
This is how I used to do it in version 5:
// v5:
<Route
path = "/blog/:id"
render = {
({match:{params}}) => <Post item={ getPostById(params.id) } />
}
/>
I am accessing the :id parameter outside the < Post /> component.
How can I write the previous code in v6?

With ({match:{params}}) => <Post item={ getPostById(params.id) } /> you've effectively written an anonymous React component that consumes a match prop and renders your Post component.
If you've components that can't use the useParams hook then write a wrapper function component that reads the id param from the useParams hook and passes the id along.
const PostWithIdParam = () => {
const { id } = useParams();
return <Post item={getPostById(id)} />;
};
...
<Route
path="/blog/:id"
element={<PostWithIdParam />}
/>

Related

How to pass id in Route React

I want to build a page when from list of products I want to see product by ID. In my App file I have something like that:
<Route path={ROUTES.product} element={<Product id={1} />}>
but I want to replace static id with the value that comes from the selected product. In my Product file I have redirection to page with product but I don't know how I can pass the ID.
onClick={() => { navigate(`/products/${product?.id}`)}}
Any help would be appreciated.
The code you've provided appears to pass the id value in the path. It seems your question more about getting the Product component to have the correct id prop passed to it.
Given: path is "/products/:id"
Options to access the id param:
Use the useParams hook in Product to read the id route path param. This only works if Product is a function component.
import { useParams } from 'react-router-dom';
...
const { id } = useParams();
...
<Route path={ROUTES.product} element={<Product />} />
Use a wrapper component to use the useParams hook and inject the id value as a prop. This is useful if Product is not a function component.
import { useParams } from 'react-router-dom';
const ProductWrapper = () = {
const { id } = useParams();
return <Product id={id} />
};
...
<Route path={ROUTES.product} element={<ProductWrapper />} />
Create a custom withRouter Higher Order Component to use the useParams hook and inject a params prop.
import { useParams, ...other hooks... } from 'react-router-dom';
const withRouter = Component => props => {
const params = useParams();
... other hooks...
return (
<Component
{...props}
params={params}
... other hooks ...
/>
);
};
...
Wrap Product with withRouter HOC and access id param from props.params
props.params.id // or this.props.params
...
export default withRouter(Product);
...
<Route path={ROUTES.product} element={<Product />} />
so you already have the id with this
navigate(`/products/${product?.id}`)
just in Product component you can access id with
const { id } = useParams();
if you need to pass extra data you can try:
navigate(`/product/${product?.id}`, { state: { someExtradata }};
that you can access state in the component with :
const { state } = useLocation();
onClick={ () => Navegar(id) }
function Navegar(id) {
navigate('/products/id)
}

get url variables from inside Route

Is there a way to get URL variables inside Route's element (and not the component itself), i.e.:
<Route
path='/book/:id'
element={<Book data={books[getUrlVarsSomehow().id]} />}
/>
This way, I can pass a single book to Book instead of passing the whole array books and choosing the correct one inside Book, which makes much more sense from a design perspective.
I am using react-router-dom v6.3.0
Yes, create a wrapper component that reads the route params (via useParams hook) and applies the filtering logic and passes the appropriate prop value.
Example:
import { useParams } from 'react-router-dom';
const BookWrapper = ({ books }) => {
const { id } = useParams();
return <Book data={books[id]} />;
};
...
<Route
path='/book/:id'
element={<BookWrapper books={books} />}
/>
react-router has a hook for that, useParams
Here is an example (from the react-router docs):
https://reactrouter.com/docs/en/v6/examples/route-objects
import { useParams } from "react-router-dom";
function Course() {
let { id } = useParams<"id">();
return (
<div>
<h2>
Welcome to the {id!.split("-").map(capitalizeString).join(" ")} course!
</h2>
<p>This is a great course. You're gonna love it!</p>
<Link to="/courses">See all courses</Link>
</div>
);
}
Live example: https://stackblitz.com/github/remix-run/react-router/tree/main/examples/route-objects?file=src/App.tsx
Create yourself a wrapping component you can use where ever you might need something from the URL:
import { useParams } from 'react-router-dom'
const WithParams = <T extends Record<string, string>>(props: {
children: (provided: T) => JSX.Element
}) => <>{props.children(useParams<T>())}</>
// usage
<Route path="/my/path/:someId">
<WithParams<{someId: string}>>
{({someId}) => (
<MyComponent id={someId} />
)}
</WithParams>
</Route>
You could create specific versions for specific path params, or create another that uses useLocation.search and parse out URL search param queries as well.

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.

Access staticContext through hook in react-router

I want to create a generic "not-found" component that sets the statusCode to 404 if the StaticRouter is used. I know that I can do this:
<Route
render={(routeComponentProps) => <NotFound {...routeComponentProps} />}
/>
Or use a child function as the doc describes, but as my app is growing in complexity I'd like to simplify it down to the way I specify all my other routes in order to avoid having to remember to pass props to the NotFound component. Something like this:
<Route>
<NotFound />
</Route>
This means I'd like to access staticContext inside <NotFound/> using a hook (preferrably), but unfortunately that does not seem possible at this time as the useRouteMatch hook does not return staticContext.
My current workaround is to grab __RouterContext from inside react-router and pass it to a useContext hook, but this seems hacky and is probably not supported. However it does work fine both server-side and client side (using the normal BrowserRouter)
import React, { useContext } from "react"
import { __RouterContext } from "react-router"
export const NotFound: React.FC = () => {
const { staticContext } = useContext(__RouterContext)
if (staticContext) {
staticContext.statusCode = 404
}
return (
<div>
<h3>404: Not found</h3>
</div>
)
}
export default NotFound
Is there a hook I can use or perhaps plans to start supporting this within the useRouteMatch hook?
I don't think that you can access staticContext inside using a documented hook. If an undocumented hook existed it would not be a better workaround than your current __RouterContext trick (which I find quite elegant already).
You could do something though, if you relax your "hook" requirement. Which is probably OK: hooks are great to reuse logic, but don't need to be usedto solve every problem.
You can access the staticContext inside your <NotFound /> without any prop: you just need to add a newcatch-all <Route /> inside.
export const NotFound: React.FC = () => {
return (
<Route
render={({ staticContext }) => {
if (staticContext) {
staticContext.statusCode = 404
}
return (
<div>
<h3>404: Not found</h3>
</div>
)
}}
>
)
}
...
<Route>
<NotFound />
</Route>

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