React-router: [v6] Discussion: Exposing some more hooks?

Created on 4 Apr 2020  路  6Comments  路  Source: ReactTraining/react-router

From: https://twitter.com/mjackson/status/1246248620023623680

Hi, I've been developing a library for React Suspense Render-as-You-Fetch with React Router.
https://github.com/dai-shi/react-suspense-router
I started with v5, but soon v6-alpha is available. It's so clean, and I like it!

Thanks to v6, my library is just a thin wrapper around it, but I need three more hooks to implement it. Here, I briefly describe them so that we can discuss any workarounds.

usePending

This is to return the pending state from useTransition. It would be used to show pending state on route change. As I think the use case is not limited to my library, but any apps in the future, I filed a PR.

https://github.com/ReactTraining/react-router/pull/7130

usePathname

As I need to patch useRoutes, it requires to get the parent pathname. This is not exposed, but I did an undesired workaround.

https://github.com/dai-shi/react-suspense-router/blob/605a05ca63d58507d02d54650b58756d2c99e634/src/Routes.tsx#L24-L26

const EMPTY_PATHNAME = { pathname: null };
const usePathname = () => useResolvedLocation(EMPTY_PATHNAME).pathname;

There might be other use cases than mine.

useListen

This is controversial. I need to call my listeners in the same startTransition in Router.
I did try various hacks without any success.

I don't know if there's any other use cases. If I were the author of RR, I wouldn't expose it. For now, I ended up with forking react-router.

https://github.com/dai-shi/react-router/compare/dev...dev-for-lib

  let listen = React.useCallback(fn => {
    childListeners.current.push(fn);
    fn(history.location); // call once with current location
    return () => {
      let index = childListeners.current.indexOf(fn);
      childListeners.current.splice(index, 1);
    };
  }, [history]);
/**
 * Returns the listen in Router (for library use)
 * Same as history.listen but called in startTransition
 */
export function useListen() {
  return React.useContext(LocationContext).listen;
}

I'm not sure if my library will be able to build on top of preload.

stale

Most helpful comment

Seconding the need for something like useListen or useAction, described above. In my app that's using v5, I setup a listener like this:

const history = useHistory();
// a placeholder function, for completeness's sake
const onRouteChange = (path) => {};

const unsubscribe = history.listen((loc, act) => {
    if (act === 'PUSH' || act === 'REPLACE') {
        const { pathname: curPathname, search } = loc;

        onRouteChange(`${curPathname}${search}`);
    }

    unsubscribe();
});

The function onRouteChange is a key-feature for my application and it's important that we only trigger it when the history method called is pushing or replacing. I don't see an alternative way to accomplish something this otherwise with the APIs provided by v6 at the moment, which prevents me from upgrading to the latest and greatest.

Otherwise, v6 looks amazing and I can't wait to upgrade to it.

All 6 comments

useHash

A useHash will also be good. It's a common use case that hash is used for controlling modals.

Usage

function App() {
  const [hash, setHash] = useHash();

  return (
    <div>
      <button onClick={() => setHash('#new')}>Open modal</button>
      <SomeFormModal show={hash === '#new'} />
    </div>
  );
}

Implementation

export function useHash() {
  const { hash, ...location } = useLocation();
  const navigate = useNavigate();

  function setHash(newHash) {
    navigate({ ...location, hash: newHash });
  }

  return [hash, setHash];
}

I'd like to contribute but I'm not good at testing. :(

Please don't move away from class-based components entirely. My experience with hooks has so far been a hellacious fiasco. I can't figure out why, but for some reason, hooks are causing all sorts of double and quadruple renders and in my scenario, multiple of the same route getting pushed onto the history stack.

@FullstackJack That is usually a sign of incorrect dependencies on useEffect or chained useState->setState calls. That's really on you to figure out, but it's not something caused by the library. I'd make sure you have eslint-plugin-react-hooks installed to ensure some safety when using them.

Access to the navigation action is needed. There is now useLocation() but no useAction() to know if the previous navigation was a push, pop or replace. This is needed in many apps for deciding how to behave when a new page comes up. E.g. restore scroll position, load data vs. use data from cache, etc.

Seconding the need for something like useListen or useAction, described above. In my app that's using v5, I setup a listener like this:

const history = useHistory();
// a placeholder function, for completeness's sake
const onRouteChange = (path) => {};

const unsubscribe = history.listen((loc, act) => {
    if (act === 'PUSH' || act === 'REPLACE') {
        const { pathname: curPathname, search } = loc;

        onRouteChange(`${curPathname}${search}`);
    }

    unsubscribe();
});

The function onRouteChange is a key-feature for my application and it's important that we only trigger it when the history method called is pushing or replacing. I don't see an alternative way to accomplish something this otherwise with the APIs provided by v6 at the moment, which prevents me from upgrading to the latest and greatest.

Otherwise, v6 looks amazing and I can't wait to upgrade to it.

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs.
You can add the fresh label to prevent me from taking any action.

Was this page helpful?
0 / 5 - 0 ratings