Skip to content

Instantly share code, notes, and snippets.

@gragland
Last active December 27, 2022 07:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save gragland/8322804ba43392d5a1e96d37d1a38218 to your computer and use it in GitHub Desktop.
Save gragland/8322804ba43392d5a1e96d37d1a38218 to your computer and use it in GitHub Desktop.
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]);
}
@gragland
Copy link
Author

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
Copy link

Naxos84 commented Jun 17, 2020

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

@Simply1993
Copy link

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
Copy link
Author

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
Copy link

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
Copy link
Author

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
Copy link

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
Copy link
Author

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

@Ridnois
Copy link

Ridnois commented Oct 23, 2020

Cannot find name 'queryString'.ts(2304) what can i do with this?

@esau-morais
Copy link

esau-morais commented Sep 17, 2022

react-router-dom is on its version 6, i'd suggest to adapt to this version

@BrunoTeixeiraNDB
Copy link

react-router-dom is on its new version 6.4, i'd suggest to adapt to this version.

@phanendraguptha
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment