Skip to content

Instantly share code, notes, and snippets.

@gragland

gragland/use-router.jsx

Last active Sep 9, 2020
Embed
What would you like to do?
React Hook recipe from https://usehooks.com
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]);
}
@HaNdTriX

This comment has been minimized.

Copy link

@HaNdTriX HaNdTriX commented Oct 22, 2019

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.

@gragland

This comment has been minimized.

Copy link
Owner Author

@gragland gragland commented Nov 6, 2019

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.

@Naxos84

This comment has been minimized.

Copy link

@Naxos84 Naxos84 commented Jun 17, 2020

import {useMemo} from "react"; is missing.

@Simply1993

This comment has been minimized.

Copy link

@Simply1993 Simply1993 commented Sep 3, 2020

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.

@gragland

This comment has been minimized.

Copy link
Owner Author

@gragland gragland commented Sep 3, 2020

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

This comment has been minimized.

Copy link

@Simply1993 Simply1993 commented Sep 4, 2020

Sure, look.
Снимок экрана 2020-09-04 в 10 30 59

If we leave only history (e.x.), the result is the same.

@gragland

This comment has been minimized.

Copy link
Owner Author

@gragland gragland commented Sep 9, 2020

@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.

@Simply1993

This comment has been minimized.

Copy link

@Simply1993 Simply1993 commented Sep 9, 2020

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]);
@gragland

This comment has been minimized.

Copy link
Owner Author

@gragland gragland commented Sep 9, 2020

@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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.