2

I have a re-usable component <Layout /> that allows me to pass in custom components into a content and sidebar property.

In one instance of this, I'm putting an input field into the content which is a controlled component and uses useState() to handle the data.

However, I'm finding that my keyboard is losing focus whenever I type into the input. It appears the entire form is re-rendering and I can't stop it from doing so.

It was fine when my code was all inline, but since refactoring it to use the <Layout /> component, it is having some strange effects.

  • I do not wish to use any extras libraries other than core React 16.x
  • I have tried using useCallback(), keys, names, ids, refs - all to no avail
  • I've moved the callback functions out of scope and passed values through as per this answer but the problem persists

import React, { useState } from 'react';

function Layout({ content: Content, sidebar: Sidebar }) {
  return (
    <>
      <div>
        <Content />
      </div>
      <div>
        <Sidebar />
      </div>
    </>
  );
}

function Page() {
  const [value, setValue] = useState('');
  return (
    <>
      <Layout
        content={() => (
          <input
            value={value}
            onChange={(event) => setValue(event.target.value)}
          />
        )}
        sidebar={() => (
          <p>Lorem ipsum...</p>
        )}
      />
    </>
  );
}

export default Page;
Matt Fletcher
  • 8,182
  • 8
  • 41
  • 60
  • Hopefully this is a useful Q&A style SO post for people. It might be too specific, I'm not sure, but it certainly stumped me on a project I'm working on and thought it might be of help to someone out there! :) – Matt Fletcher May 13 '20 at 16:48
  • Try it at: https://repl.it/repls/AbleFrugalParticles – Matt Fletcher May 13 '20 at 16:51

2 Answers2

2

Your problem is that you are defining anonymous functions and treating them as React components, which is fine for many cases, but here is causing React to lose its understanding of the underlying content within those functions. This means that even adding refs or keys to the input will do nothing, as the outer scope just sees a brand new function each time.

Consider for a second: instead of writing <Content />, you instead write <>{Content()}</>

This will get the same outcome, but shows that each time Layout is rendered it has an entirely different view of what its properties are, and thus renders the whole thing afresh each time. Because the input is being replaced quickly by a new but different input, the browser loses focus in the box. But it can also cause a tonne of other issues.

Instead, replace <Content /> with {content} and don't pass an anonymous function through- instead just pass the raw JSX as it is and it will no longer re-render the children.

import React, { useState } from 'react';

function Layout({ content, sidebar }) {
  return (
    <>
      <div>
        {content}
      </div>
      <div>
        {sidebar}
      </div>
    </>
  );
}

function Page() {
  const [value, setValue] = useState('');
  return (
    <>
      <Layout
        content={(
          <input
            value={value}
            onChange={(event) => setValue(event.target.value)}
          />
        )}
        sidebar={(
          <p>Lorem ipsum...</p>
        )}
      />
    </>
  );
}

export default Page;
Matt Fletcher
  • 8,182
  • 8
  • 41
  • 60
0

The problem is that when Content is rendered as a react component inside Layout ,every-time Page re-renders, a new instance of content and sidebar functions are created, due to this react remounts the component as the function references are changed, because functions are recreated on every render inside react component and therefore the input focus is lost.

You can use content and sidebar components as render prop. A render prop is a function prop that a component uses to know what to render. Call the functions inside Layout to render Content and Sidebar.

function Layout({ content: Content, sidebar: Sidebar }) {
  return (
    <>
      <div>{Content()}</div>
      <div>{Sidebar()}</div>
    </>
  );
}

function Page() {
  const [value, setValue] = useState("");

  return (
    <>
      <Layout
        content={() => (
          <input
            value={value}
            onChange={event => setValue(event.target.value)}
          />
        )}
        sidebar={() => <p>Lorem ipsum...</p>}
      />
    </>
  );
}
Jagrati
  • 11,474
  • 9
  • 35
  • 56