Search code examples
reactjstypescriptdesign-patternsreact-reduxmemoization

How do I prevent an input in a list to rerender (and loose focus) on input, using React functional components?


I have a list of items (openItems) that I want to render in a list. For each item in the list I need an input that changes one property (duration) of the open item. The problem is that every time I change a property, the parent (openItems), and all its children (including the input) rerenders. This makes the input loose focus.

I'm trying to wrap my head around how this pattern should be implemented (using functional components) in order to not break. I tried using React.memo, but as the data changes this makes no difference.

Below is a stripped down code snippet of how it's currently implemented. (I'm using Antd components)

const Page = () => {
  const dispatch = useDispatch();
  const openItems = ...get items from state

  const setMs = (id: string, ms: number) => {
    dispatch(changeDuration({ id, duration: ms }));
  }

  const InputMs: React.FC<{ row: RowInterface }> = (props) => (
    <InputNumber
      defaultValue={props.row.durationMs}
      onChange={(v) => setMs(props.row.id, v)}
    />
  )

  const RowContent: React.FC<{ row: RowInterface }> = (props) => (
    <>
      <SmallText>Duration in ms:</SmallText>
      <InputMs row={props.row} />
    </>
  );

  const Rows: React.FC<RowProps> = (props) => {
    return (
      <>
        {props.data.map((row) =>
          <RowContent row={row} />
        )}
      </>
    )
  };

  return( <Rows data={openItems} /> )
};

export default Page;

Based on this answer, I've also tried making the input a separate component, but with the same result:

const InputMs: React.FC<{ row: RowInterface  }> = (props) => {
  const dispatch = useDispatch();
  const setMs = (id: string, ms: number) => {
    dispatch(changeDuration({ id, duration: ms }));
  }
  return (
    <InputNumber
      defaultValue={props.row.durationMs}
      onChange={(v) => setMs(props.row.id, v)}
    />
  )
}

const Page = () => {
  const openItems = ...get items from state;
  const RowContent: React.FC<{ row: RowInterface  }> = (props) => (
    <>
      <SmallText>Duration in ms:</SmallText>
      <InputMs row={props.row} />
    </>
  );

  const Rows: React.FC<RowProps> = (props) => {
    return (
      <>
        {props.data.map((row) =>
          <RowContent row={row} />
        )}
      </>
    )
  };

  return (<Rows data={openItems} />)
};

export default Page;


Solution

  • What is happening here is that you are defining sub-components inside of another component. This means that when Page re-renders due to changes in the redux state, your InputMs, RowContent, and Rows components all get re-created. They will unmount and remount and lose their focused state, as it is now an entirely new component instance.

    You're halfway there because you found already found the linked answer which tells you this. But you must move all of the inner components. You only moved InputMs, but you are still calling it inside of Rows and RowContent which are getting re-created.

    You want to define all components at the top-level of the file.

    It is also possible to write these snippets as plain functions that you call as functions rather than calling them as JSX elements. For example:

    const Page = () => {
      const openItems = ...get items from state;
    
      const renderRow = (row: RowInterface) => (
        <>
          <SmallText>Duration in ms:</SmallText>
          <InputMs row={row} />
        </>
      );
    
      return (
        <>
          {openItems.map(renderRow)}
        </>
      );
    }