简体   繁体   中英

React Router - How to prevent top level refresh when child component button is clicked, preventDefault does not seem to make a difference

Background:

I have a simple react router:

<BrowserRouter >
    <Routes>
      <Route path="test/:locationId" element={<TestComponent />}></Route>

Then I have the component really simple:

function Inner(props) {
  let ele = JSON.stringify(props);

  console.log(" this is inner , with " + ele);

  const [value, setValue] = useState(parseInt(ele));

  return (
    <div onClick={(e) => e.stopPropagation()}>
      <p> Inner component: {String(ele)}</p>
      <button
        onClick={(e) => {
          e.preventDefault();
          e.stopPropagation();
          console.log("inner btn clicked for " + value);
          setValue(value + 1);
        }}
      >
        {" "}
        inner btn with clicked value {value}
      </button>
    </div>
  );
}

export default function TestComponent(props) {
  console.log("TestComponent top level component " + new Date());
  console.log("I will do some heavy work like a big http request.....")
  const [res, setRes] = useState([1, 2, 3]);
  let params = useParams();
  console.dir(params);
  
  if (res.length > 0) {
    console.log("res > 0 ");
    return (
      <div className="container">
        {res.map((ele) => {
          return <div key={String(ele)}>{Inner(ele)}</div>;
        })}
      </div>
    );
  }

Problem: Anytime I click on any of the 3 buttons, I see TestComponent refreshes from the top level, like a new line of TestComponent top level component Mon Jul 18 2022 17:35:52 GMT-0400 (Eastern Daylight Time) .

Now I plan to do some heavy http request and setState() inside TestComponent . However whenever I click on the buttons (in child components) this TestComponent always refreshes and thus will do the http request over and over again which I want to prevent. Tried e.stopPropagation() and e.preventDefault() but saw no difference. enter image description here

Issues

It doesn't seem you understand the React component lifecycle well and are using the completely wrong tool to measure React renders.

  1. All the console logs are occurring outside the render cycle as unintentional side-effects. What this means is that anytime React calls your app ( the entire app code ) to rerender ( for any reason ) that the component body is executed during the "render phase" in order to compute a diff for what changed and actually needs to be pushed to the DOM during the "commit phase". The commit phase is React rendering the app to the DOM and this is what we often refer colloquially as the React component rendering.

    在此处输入图像描述

    Note that the "render phase" is to be considered a pure function that can be called anytime React needs to call it. This is why it's pure function, ie without unintentional side-effects. The entire function body of a React function component is the "render" method.

    Note the "commit phase" is where the UI has been updated to the DOM and effects can now run.

    The issue of using the console.log outside the component lifecycle is exacerbated usually by the fact that we render our apps into the React.StrictMode component that does some double mounting of apps/components to ensure reusable state , and intentionally double invokes certain methods and functions as a way to detect unintentional side-effects .

  2. Inner is defined like a React component but called manually like a regular function. This is a fairly obvious no-no. In React we pass a reference to our React components to React as JSX . We don't ever directly call our React functions directly.

Solution/Suggestion

  • Move all the console.log statements into useEffect hooks so they are called exactly once per render ( to the DOM ) cycle. All intentional side-effects should be done from the useEffect hook.
  • Render Inner as a React component and correctly pass props to it.

Example:

function Inner({ ele }) {
  useEffect(() => {
    console.log("this is inner , with " + ele);
  });

  const [value, setValue] = useState(ele);

  return (
    <div onClick={(e) => e.stopPropagation()}>
      <p>Inner component: {String(ele)}</p>
      <button
        onClick={(e) => {
          e.preventDefault();
          e.stopPropagation();
          console.log("inner btn clicked for " + value);
          setValue(value + 1);
        }}
      >
        inner btn with clicked value {value}
      </button>
    </div>
  );
}

...

function TestComponent(props) {
  const [res, setRes] = useState([1, 2, 3]);
  let params = useParams();

  useEffect(() => {
    console.log("TestComponent top level component " + new Date());
    console.log("I will do some heavy work like a big http request.....");

    console.dir(params);

    if (res.length > 0) {
      console.log("res > 0 ");
    }
  });

  if (res.length > 0) {
    return (
      <div className="container">
        {res.map((ele) => (
          <div key={ele}>
            <Inner {...{ ele }} />
          </div>
        ))}
      </div>
    );
  }
}

...

<Routes>
  <Route path="test/:locationId" element={<TestComponent />} />
</Routes>

在此处输入图像描述

Note that after the initial StrictMode mounting/remount that clicking the buttons only logs a single log from each Inner component.

编辑 react-router-how-to-prevent-top-level-refresh-when-child-component-button-is-c

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM