skip to Main Content

My intention is to create a custom input element that avoids re-rendering whenever the input value changes, but at the same time notifies the parent if the input changes.

Input component:

import { useRef, useEffect, ChangeEvent } from "react";

type props = {
changeFunction?: (e: ChangeEvent<HTMLInputElement>) => void;
};

function TestInput({ changeFunction }: props) {
let inputRef = useRef<HTMLInputElement>(null);

useEffect(() => {
  console.log("Input render");
});

function change(e: ChangeEvent<HTMLInputElement>) {
  console.log("Input change", e.target.value);
  if (inputRef.current) inputRef.current.value = e.target.value;
  if (changeFunction) changeFunction(e);
}

return (
  <input
    ref={inputRef}
    type="search"
    id="test"
    name="test"
    autoComplete="off"
    onChange={change}
  />
);
}

export default TestInput;

Parent component:

import { useCallback, useState, ChangeEvent } from "react";
import TestInput from "./Test";

function TestParent() {
  let [value, setValue] = useState("");
  let change = useCallback(
    (e: ChangeEvent<HTMLInputElement>) => setValue(e.target.value),
    []
  );

  return (
    <div>
      <p>Value: {value}</p>
      <TestInput changeFunction={change} />
    </div>
  );
}

export default TestParent;

Despite using useCallback the input component re-renders every time. If we exclude the changeFunction prop this doesn’t happen. What could be the solution?

Here’s a Stack Snippet without the TypeScript parts showing the problem.

const { useState, useEffect, useCallback, useRef } = React;

function TestInput({ changeFunction }) {
  let inputRef = useRef(null);

  useEffect(() => {
    console.log("Input render");
  });

  function change(e) {
    console.log("Input change", e.target.value);
    if (inputRef.current) inputRef.current.value = e.target.value;
    if (changeFunction) changeFunction(e);
  }

  return (
    <input
      ref={inputRef}
      type="search"
      id="test"
      name="test"
      autoComplete="off"
      onChange={change}
    />
  );
}

function TestParent() {
  let [value, setValue] = useState("");
  let change = useCallback(
    (e) => setValue(e.target.value),
    []
  );

  return (
    <div>
      <p>Value: {value}</p>
      <TestInput changeFunction={change} />
    </div>
  );
}

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<TestParent />);
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>

2

Answers


  1. You could use React.memo. Because input props are not changing the input will not be re-rendered when its parent state changes.

    export default React.memo(TextInput);
    
    const { memo, useState, useEffect, useCallback, useRef } = React;
    
    function TestInput({ changeFunction }) {
      let inputRef = useRef(null);
    
      useEffect(() => {
        console.log("Input render");
      });
    
      function change(e) {
        console.log("Input change", e.target.value);
        if (inputRef.current) inputRef.current.value = e.target.value;
        if (changeFunction) changeFunction(e);
      }
    
      return (
        <input
          ref={inputRef}
          type="search"
          id="test"
          name="test"
          autoComplete="off"
          onChange={change}
        />
      );
    }
    
    const MemoTestInput = memo(TestInput);
    
    function TestParent() {
      let [value, setValue] = useState("");
      let change = useCallback(
        (e) => setValue(e.target.value),
        []
      );
    
      return (
        <div>
          <p>Value: {value}</p>
          <MemoTestInput changeFunction={change} />
        </div>
      );
    }
    
    const root = ReactDOM.createRoot(document.getElementById("root"));
    root.render(<TestParent />);
    <div id="root"></div>
    
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
    Login or Signup to reply.
  2. Rerendering of the child component is not something you need to avoid. That is how react works out the changes necessary to be made to the dom. So unless your use case specifically requires the child component to not rerender you should avoid these kinds of perfomance optimizations, that said.

    You could Memoize the child component

    import { memo } from 'react';
    
    // ...
    
    export default memo(TestInput);
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search