import { useMemo } from "react"; | |
import { useParams, useLocation, useHistory, useRouteMatch } from 'react-router-dom'; | |
import queryString from 'query-string'; | |
// Usage | |
function MyComponent(){ | |
// Get the router object | |
const router = useRouter(); | |
// Get value from query string (?postId=123) or route param (/:postId) | |
console.log(router.query.postId); | |
// Get current pathname | |
console.log(router.pathname) | |
// Navigate with with router.push() | |
return ( | |
<button onClick={(e) => router.push('/about')}>About</button> | |
); | |
} | |
// Hook | |
export function useRouter() { | |
const params = useParams(); | |
const location = useLocation(); | |
const history = useHistory(); | |
const match = useRouteMatch(); | |
// Return our custom router object | |
// Memoize so that a new object is only returned if something changes | |
return useMemo(() => { | |
return { | |
// For convenience add push(), replace(), pathname at top level | |
push: history.push, | |
replace: history.replace, | |
pathname: location.pathname, | |
// Merge params and parsed query string into single "query" object | |
// so that they can be used interchangeably. | |
// Example: /:topic?sort=popular -> { topic: "react", sort: "popular" } | |
query: { | |
...queryString.parse(location.search), // Convert string to object | |
...params | |
}, | |
// Include match, location, history objects so we have | |
// access to extra React Router functionality if needed. | |
match, | |
location, | |
history | |
}; | |
}, [params, match, location, history]); | |
} |
Please don't do this. The react-router team splitted these hooks by propose.
I mentioned in the post description that there are performance concerns and that it makes sense that react-router would split hooks. That said, is this really that bad? Next.js has a single useRouter
hook for the same data and I assume that was a tradeoff they felt made sense.
I generally prefer the better DX and would only want to drop down into lower-level hooks for specific cases where an extra re-render could lead to performance problems (which is pretty rare for me, even with complex deeply nested apps). Maybe I'm missing something though. Don't want to lead people astray if there are serious issues with this method.
import {useMemo} from "react";
is missing.
If we use the library why-did-you-render, then we will see re-renders because of this hook.
Reason for this, nested objects - params, location, history, match.
If we use the library why-did-you-render, then we will see re-renders because of this hook.
Reason for this, nested objects - params, location, history, match.
Since it uses useMemo
it should only return a new object if one of those nested objects changes. Could you clarify the issue you're seeing?
@Simply1993 Since this hook calls useLocation
it will cause all components that utilize it to re-render on route change. That can be unnecessary if a component is only importing useRouter
to call router.push
. If those extra re-renders are a performance problem then I'd recommend just using the React Router hooks directly. Let me know if you think something else is going on here though.
I said:
If we leave only history (e.x.), the result is the same.
As far as I know, useMemo does not check equal for deep objects.
So you have to do something like this:
const location = useLocation();
const [locationState, setLocationState] = useState(location);
const history = useHistory();
const [historyState, setHistoryState] = useState(history);
const match = useRouteMatch();
const [matchState, setMatchState] = useState(match);
useEffect(() => {
if (!isEqual(locationState, location)) {
setLocationState(location);
}
}, [location]);
useEffect(() => {
if (!isEqual(historyState, history)) {
setHistoryState(history);
}
}, [history]);
useEffect(() => {
if (!isEqual(matchState, match)) {
setMatchState(match);
}
}, [match]);
return useMemo(() => ({
history: historyState,
match: matchState,
location: locationState,
}), [historyState, matchState, locationState]);
@Simply1993 The result would be the same if you used the useHistory hook directly right? useRouter doesn't cause any re-renders itself because it doesn't set state. To the degree that there are re-renders and new object references are returned, that's a decision made by the underlying React Router hooks (or a bug that should be reported with them).
Cannot find name 'queryString'.ts(2304)
what can i do with this?
react-router-dom is on its version 6, i'd suggest to adapt to this version
react-router-dom is on its new version 6.4, i'd suggest to adapt to this version.
@gragland I updated the hook with the react-router-dom latest version 6.
import { useMemo } from "react";
import {
useParams,
useLocation,
useNavigate,
useSearchParams,
} from "react-router-dom";
// Usage
function MyComponent() {
// Get the router object
const router = useRouter();
// Get value from query string (?postId=123) or route param (/:postId)
console.log(router.query.postId);
// Get current pathname
console.log(router.pathname);
// Navigate with router.navigate()
return <button onClick={(e) => router.navigate("/about")}>About</button>;
}
export function useRouter() {
const params = useParams();
const location = useLocation();
const [searchParams] = useSearchParams();
const navigate = useNavigate();
let queryObj = {};
for (const [key, value] of searchParams.entries()) {
queryObj[key] = value;
}
// Return our custom router object
// Memoize so that a new object is only returned if something changes
return useMemo(() => {
return {
pathname: location.pathname,
// Merge params and parsed query string into single "query" object
// so that they can be used interchangeably.
// Example: /:topic?sort=popular -> { topic: "react", sort: "popular" }
query: {
...params,
...queryObj,
},
// Include location, navigate objects so we have
// access to extra React Router functionality if needed.
location,
navigate,
};
}, [params, location, navigate]);
}
export default MyComponent;
Please don't do this. The react-router team splitted these hooks by propose.
Some of these hooks never change others may change quite often.
If a component just needs a react-router hook that doesn't change it will be rerendered by the
useRouter
hook above, even though it doesn't need to.I've already seen many people implementing the
useRouter
without knowing the price of it.