Search code examples
reactjsreact-routerreact-router-dom

How to implement listen function in react-router-dom v6?


I'm updating a react project's packages, implemented with react-router-dom v5. I've updated react-router-dom to v6. I've used this.props.history in a lot of components, and because of that, I don't need to change its structure, so I decided to create a wrapper that has that functionality and pass the components to it as a prop to don't change a lot of code. Here's the wrapper:

import * as React from 'react';
import { useLocation, useNavigate, useNavigationType, useParams } from "react-router";

export function withRouter(Child) {
  return (props) => {
    const [callBack,setCallBack] = React.useState();
    const location = useLocation();
    const navigate = useNavigate();
    const action = useNavigationType();
    const params = useParams();
    const match = {
      params: params
    }
    const history = {
      push: (url, onMount) => {
        if (onMount)
          setTimeout(() => navigate(url), 100)
        else
          navigate(url);

      },
      listen: (callback) => {
        setCallBack(callback);
      }
    }
    React.useEffect(() => {
      if (callBack) {
        callBack(location,action)
      }
    }, [location])

    return <Child {...props} history={history} location={location} match={match} />;
  }
}

I've used my wrapper in a component like this:

function action(params) {
    return (
        <Layout {...params}>
            <PageInfo title={Utils.i18n('notificationList', true)} icon="ti-bell"/>
            <NotificationList {...params}/>
        </Layout>
    )
}

export default withRouter(action);

And Here's the listen function on componentDidMount of the wrapped component:

componentDidMount(): void {
        this.props.history.listen((location, action) => {
            const urlSearchParams = new URLSearchParams(window.location.search);
            const params = Object.fromEntries(urlSearchParams.entries());
            if (params.id) {
                this.setState({id:params.id})
            }
        });
        this.getNotification();
        Utils.scrollToTop();
 }

My problem in this update is the listen functionality, which existed in react-router-dom v5. Because after any state change in the Child prop, all states cleared, so callBack is always undefined, and I couldn't implement listen functionality in my wrapper. In this part of the code, the callback is always undefined:

 React.useEffect(() => {
      if (callBack) {
        callBack(location,action)
      }
    }, [location])

Does anyone know how to implement my wrapper's listen functionality?


Solution

  • The only overt issue I see here are missing useEffect dependencies. callback and action are both missing.

    React.useEffect(() => {
      if (callback) {
        callback(location, action);
      }
    }, [action, callback, location]);
    

    A much more covert issue is saving a function into React state via the setState updater function. The setState function accepts a value to update the state or a callback function that is passed the previous state value and is expected to return the next state value. If you want to save a callback function into state you'll need to use the callback function syntax and return the callback function you want to save into state.

    listen: (callback) => {
      setCallback(() => callback); // not setCallback(callback)
    }
    

    Full code:

    export const withRouter = Component => props => {
      const [callback, setCallback] = React.useState();
      const location = useLocation();
      const navigate = useNavigate();
      const action = useNavigationType();
      const params = useParams();
    
      const match = { params };
    
      const history = {
        push: (url, onMount) => {
          if (onMount) {
            setTimeout(() => navigate(url), 100);
          } else {
            navigate(url);
          }
        },
        listen: (callback) => {
          setCallback(() => callback);
        }
      }
    
      React.useEffect(() => {
        if (callback) {
          callback(location, action);
        }
      }, [action, callback, location]);
    
      return <Component {...props} {...{ history, location, match }} />;
    };