Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to handle react History.Push() causing undesired component remounts?

Problem

Calling history.push() inside of a component seems to cause the entire react component to unmount and remount; causing pointless remote service calls.

Specifically, I have a remote service call that fires on component entry. I do not want the component to remount, and I don't want the service call to re-run (its slow).

It seems like history.push(location.pathname + '?' + encodeURI(urlSearchParams.toString())); is going to cause an unmount no matter what. Am I using it incorrectly? Is there a better approach for tracking the history of a users' filter changes and not having to worry about extraneous service calls?

Intent

I'm utilizing history.push() to keep the browser history updated with changes to the query params. The query params control filtering of table data, e.g. ?sort=asc&isCompleted=true, etc.

When a user changes their filtering settings, I intend for the existing table data stored in state to simply be filtered, rather than re-loading the data remotely and forcing the user to sit and wait. I also want a user to be able to share a URL to another user with the appropriate filters included.

What I've Tried

  • Tried dropping history.push() altogether, using only state. This works, but means its not possible to have a sharable URL with the filters appended as query params.
  • Tried tinkering with useEffect() and useRef() but was frustrated by the incessant remounting.

Component Code

import React, { useEffect, useState } from 'react';
import { useLocation, useHistory } from 'react-router-dom';

function useQuery() {
  return new URLSearchParams(useLocation().search);
}

export const WidgetTable = () => {
  let urlSearchParams = useQuery();
  let history = useHistory();
  let location = useLocation();

  const [originalTableData, setOriginalTableData] = useState<TableData| undefined>(undefined);
  const [filteredTableData, setFilteredTableData] = useState<TableData| undefined>(undefined);

  // go get the table data from the remote service
  const fetchTableData = async () => {
   <- go remotely fetch table data and then set originalTableData ->
  }

  // triggered when a user sets a filter on the table (updates the data displayed in the table)
  const filterTableData = () => {
   <- filter the existing table data in state and then set the filterdTableData ->
  }

  // also triggered when a user sets a filter on the table (updates the URL in the browser)
  const setFilter = (filterToSet: ReleasePlanFilterType, value: string) => {
    switch (filterToSet) {
      case ReleasePlanFilterType.Target: {
        if (urlSearchParams.get(filterToSet)) {
          urlSearchParams.set(filterToSet, value);
        } else {
          urlSearchParams.append(filterToSet, value);
        }
        break;
      }
      <snip>
    }

   // We've set the filter in the query params, but persisting this to the history causes a reload :(
   history.push(location.pathname + '?' + encodeURI(urlSearchParams.toString())); 
  
  }

  useEffect(() => {
    fetchTableData();
  }, []);

  return (<snip> a fancy table and filtering controls <snip>);

}

like image 789
Ray Avatar asked Jul 15 '20 22:07

Ray


2 Answers

From what I can tell, after reading through several other similar stack overflow questions, there doesn't seem to be a way to not remount (rerender) the component. The history changes automatically causes a prop change from how react router dom handles things under the hood. Most of these questions that I find used class components and the answer was to use shouldComponentUpdate to see if the previous path was equal to the new path. If they were equal then they would return, essentially making it so they wouldn't rerender.

shouldComponentUpdate: function(nextProps, nextState) {
  if(this.props.route.path == nextProps.route.path) return false;
  return true;
}

Source: Prevent react-router history.push from reloading current route

So now the issue would be converting this to work with hooks. I'll need to run some tests, but I believe this should work for your use case.

useEffect(() => {
    fetchTableData();
}, [location.pathname]);

By adding location.pathname as a dependency, this effect should then only be called when the pathname actually changes.

like image 191
Chris Avatar answered Oct 07 '22 13:10

Chris


After hours and hours of debugging I managed to solve this by using:

<Route exact path="/">
  <Home />
</Route>

Instead of:

<Route exact path="/" component={Home} />

This works with react-router v5.

like image 4
Wojtek Avatar answered Oct 07 '22 11:10

Wojtek