Home > OS >  useState affects all instances of a Component
useState affects all instances of a Component

Time:08-05

I have a simple component below named Idea:

import './Idea.css';
import { useState } from 'react';
import { useParams } from 'react-router-dom';

function Idea() {
  const { id } = useParams();
  const [ test, setTest ] = useState('');

  console.log('Idea rendered');

  const handleChange = (event) => {
    setTest(event.target.value);
  };
  
  return (
    <form>
      <input onChange={handleChange} value={test}/>
    </form>
  );
};

export default Idea;

Each instance of this component is rendered using react-router via dynamic routing, as shown in the second route component below:

function App() {
  return (
    <Provider store={store}>
      <BrowserRouter>
        <main>
          <SideBar />
          <section id='main-window'>
            <Routes>
              <Route exact path="/" element={ <Instructions /> } />
              <Route path="/ideas/:id" element={ <Idea/> } />
            </Routes>
          </section>
        </main>
      </BrowserRouter>
    </Provider>
  );
};

The problem is illustrated in this example: Let's say I am on route /ideas/1234 and its input value says hello world!. If I switch to route /ideas/5678 then the input on that route will also say hello world! even though I specified the input value to say otherwise upon render. If I change the input value on route /ideas/5678 the input value on route /input/1234 will change as well. They will inevitably both be the same value as if they're the same, regardless of the route. This started happening when I introduced the useState hook in the Idea component. I assume useState should reset the input value every time I change a route but this does not seem to happen. Any help would be appreciated.

CodePudding user response:

Here's the documentation for the useState hook: https://reactjs.org/docs/hooks-state.html

Here's the important part for your case:

What does calling useState do? It declares a “state variable”. ... This is a way to “preserve” some values between the function calls ... Normally, variables “disappear” when the function exits but state variables are preserved by React.

So by introducing useState hook to the Idea component, you are actually preserving the input value. If you want it to clear out based on a certain condition, like if the value of id from useParams has changed, look into useEffect hook. Maybe do something like:

useEffect(()=> {
    setTest('');
}, [id]);

CodePudding user response:

Given route <Route path="/ideas/:id" element={<Idea />} /> when the route path param id changes the Route component doesn't unmount and mount a new instance of Idea as this would be wasteful, and instead it will simply rerender Idea with a new route path parameter provided from a routing context.

Think of this like any other React component where they rerender for one of three reasons:

  1. A state update was enqueued and trigger a component rerender.
  2. A prop value updated and triggered a component rerender.
  3. A component's parent component rerendered.

Your code is in the third case where the URL path changed and the Routes component matches the best Route component, and since <Route path="/ideas/:id" element={<Idea />} /> was already matched it rerenders, which rerenders Idea.

If the routed component needs to handle changes to the route params then it should use the useEffect hook to "listen" for them.

Example:

function Idea() {
  const { id } = useParams();
  const [test, setTest] = useState('');

  useEffect(() => {
    console.log('Idea rendered');
  });

  useEffect(() => {
    console.log('id updated', { id });
    setTest('');
  }, [id]);

  const handleChange = (event) => {
    setTest(event.target.value);
  };
  
  return (
    <form>
      <input onChange={handleChange} value={test} />
    </form>
  );
};

An alternative to listening to changes is to create a wrapper component that uses the route param as a React key on the routed component. Using this method will unmount and mount a new component instance.

Example:

const IdeaWrapper = () => {
  const { id } = useParams();
  return <Idea key={id} />;
};

...

<Routes>
  <Route exact path="/" element={<Instructions />} />
  <Route path="/ideas/:id" element={<IdeaWrapper />} />
</Routes>

If you wanted, you could even create a custom state hook that resets on dependency updates.

Example:

const useResetState = (initialState, deps) => {
  const [state, setState] = useState(initialState);
  useEffect(() => {
    setState(initialState);
  }, deps);

  return [state, setState];
};

...

function Idea() {
  const { id } = useParams();

  const [test, setTest] = useResetState('', [id]); // <-- resets when id updates

  useEffect(() => {
    console.log('Idea rendered');
  });

  const handleChange = (event) => {
    setTest(event.target.value);
  };
  
  return (
    <form>
      <input onChange={handleChange} value={test} />
    </form>
  );
};
  • Related