Mohit K.
Mohit K.

Reputation: 61

Update a react component only on button click with hooks

Application:

  1. Search bar with two text input fields (input1, input2)
  2. Three buttons: SearchX, SearchY, Clear results
  3. Both the searches can take input1 and input2 as parameters to give two different results.
  4. There's a result component which takes both the inputs, action and renders the search component depending on the action.
function TestComponent() {
  const [input1, setInput1] = useState('');
  const [input2, setInput2] = useState('');
  const [action, setAction] = useState(null);

  const onInput1Change = evt => setInput1(evt.target.value);
  const onInput2Change = evt => setInput2(evt.target.value);

  return (
    <div>
      <input type="text" value={input1} onChange={onInput1Change} />
      <input type="text" value={input2} onChange={onInput2Change} />
      <button type="button" onClick={() => setAction('SearchX')}>
        SearchX
      </button>
      <button type="button" onClick={() => setAction('SearchY')}>
        SearchY
      </button>
      <button type="button" onClick={() => setAction('Clear results')}>
        Clear results
      </button>
      <ResultComponent input1={input1} input2={input2} action={action} />
    </div>
  );
}

function ResultComponent({ input1, input2, action }) {
  if (action === 'SearchX') {
    return <SearchX input1={input1} input2={input2} />;
  }

  if (action === 'SearchY') {
    return <SearchY input1={input1} input2={input2} />;
  }

  if (action === 'Clear results') {
    return null;
  }

  return null;
}

function SearchX({ input1, input2 }) {
  const [result, setResult] = useState(null);

  useEffect(() => {
    // Fetch and process X-way to get the result. Using timeout to simulate that
    const id = window.setTimeout(() => setResult(`Search X result with inputs: ${input1}, ${input2}`), 3000);
    return () => window.clearInterval(id);
  }, [input1, input2]);

  return <div>{result}</div>;
}

function SearchY({ input1, input2 }) {
  const [result, setResult] = useState(null);

  useEffect(() => {
    // Fetch and process Y-way to get the result. Using timeout to simulate that
    const id = window.setTimeout(() => setResult(`Search Y result with inputs: ${input1}, ${input2}`), 3000);
    return () => window.clearInterval(id);
  }, [input1, input2]);

  return <div>{result}</div>;
}

ReactDOM.render(<TestComponent />, document.getElementById('root'));

Problem:

We want the search to initiate only when a button is clicked. With below code, after the first search result, as soon as you change your input, the result component expectedly re-renders thereby initiating search again without button click

Steps to reproduce the problem:

  1. Enter "input1" in first text box
  2. Enter "input2" in second text box
  3. Hit on "SearchX"
  4. After 3 seconds you should see something like "Search X result with inputs: input1, input2"
  5. Change any of the input boxes. Need not press enter.
  6. After 3 seconds, the result would change without button click

Possible option:

Planning to use React.memo hook to compare action prop before updating the result component. Action prop can only change on button clicks and hence can solve the problem.

Question:

  1. Is there any other way (any other hooks etc.) to solve the problem?
  2. Or is there any other process/design that I can follow to avoid memo ?

Upvotes: 1

Views: 2613

Answers (2)

Drew Reese
Drew Reese

Reputation: 202608

You could, upon input interaction, reset the action back to null. This will clear out the current result and not trigger a "search".

function TestComponent() {
  const [input1, setInput1] = useState('');
  const [input2, setInput2] = useState('');
  const [action, setAction] = useState(null);

  const onInput1Change = evt => {
    setInput1(evt.target.value);
    setAction(null);
  };
  const onInput2Change = evt => {
    setInput2(evt.target.value)
    setAction(null);
  };

  return (
    <div>
      <input type="text" value={input1} onChange={onInput1Change} />
      <input type="text" value={input2} onChange={onInput2Change} />
      <button type="button" onClick={() => setAction('SearchX')}>
        SearchX
      </button>
      <button type="button" onClick={() => setAction('SearchY')}>
        SearchY
      </button>
      <button type="button" onClick={() => setAction(null)}>
        Clear results
      </button>
      <ResultComponent input1={input1} input2={input2} action={action} />
    </div>
  );
}

EDIT Use html5 forms to save input and set action upon submit. When inputs are interacted with the inputs in state aren't updated until form is submitted.

function TestComponent() {
  const [input1, setInput1] = useState("");
  const [input2, setInput2] = useState("");
  const [action, setAction] = useState(null);

  return (
    <div>
      <form
        id="searchX"
        onSubmit={e => {
          e.preventDefault();
          setInput1(e.target.inputX.value);
          setAction("SearchX");
        }}
      />
      <form
        id="searchY"
        onSubmit={e => {
          e.preventDefault();
          setInput2(e.target.inputY.value);
          setAction("SearchY");
        }}
      />

      <input id="inputX" form="searchX" type="text" />
      <input id="inputY" form="searchY" type="text" />

      <input form="searchX" type="submit" value="SearchX" />
      <input form="searchY" type="submit" value="SearchY" />
      <button type="button" onClick={() => setAction(null)}>
        Clear results
      </button>

      <ResultComponent input1={input1} input2={input2} action={action} />
    </div>
  );
}

Edit amazing-cori-97g7e

Also, setting the "clear results" button action back to null saves a conditional check in ResultComponent, which simplifies to:

function ResultComponent({ input1, input2, action }) {
  if (action === 'SearchX') {
    return <SearchX input1={input1} input2={input2} />;
  }

  if (action === 'SearchY') {
    return <SearchY input1={input1} input2={input2} />;
  }

  return null;
}

Upvotes: 1

strdr4605
strdr4605

Reputation: 4352

You can use refs to inputs and only update state on button click.

export default function TestComponent() {
  const [input1, setInput1] = useState("");
  const [input2, setInput2] = useState("");
  const [action, setAction] = useState(null);

  const input1Ref = useRef(null);
  const input2Ref = useRef(null);

  const onButtonClick = () => {
    if (input1Ref.current) {
      setInput1(input1Ref.current.value);
    }
    if (input2Ref.current) {
      setInput2(input2Ref.current.value);
    }
  };

  const onSearchXClick = () => {
    onButtonClick();
    setAction("SearchX");
  };

  const onSearchYClick = () => {
    onButtonClick();
    setAction("SearchX");
  };

  return (
    <div>
      <input ref={input1Ref} type="text" />
      <input ref={input2Ref} type="text" />
      <button type="button" onClick={onSearchXClick}>
        SearchX
      </button>
      <button type="button" onClick={onSearchYClick}>
        SearchY
      </button>
      <button type="button" onClick={() => setAction("Clear results")}>
        Clear results
      </button>
      <ResultComponent input1={input1} input2={input2} action={action} />
    </div>
  );
}

Edit silly-maxwell-usrrj

Upvotes: 0

Related Questions