Search code examples
reactjsreact-hooks

React Hooks - Input loses focus when 1 character is typed in


I'm playing with React Hooks - rewriting a form to use hook concepts. Everything works as expected except that once I type any 1 character into the input, the input loses focus.

I guess there is a problem that the outside of the component doesn't know about the internal changes in the component, but how do I resolve this issue?

Here is the useForm Hook:

import React, { useState } from "react";

export default function useForm(defaultState, label) {
  const [state, setState] = useState(defaultState);

  const FormComponent = () => (
    <form>
      <label htmlFor={label}>
        {label}
        <input
          type="text"
          id={label}
          value={state}
          placeholder={label}
          onChange={e => setState(e.target.value)}
        />
      </label>
    </form>
  );

  return [state, FormComponent, setState];
}

Here is the component that uses the Hook:

function App() {
  const [formValue, Form, setFormValue] = useForm("San Francisco, CA", "Location");

  return (
    <Fragment>
      <h1>{formValue}</h1>
      <Form />
    </Fragment>
  );
}

Solution

  • While the answer by Kais will solve the symptoms, it will leave the cause unaddressed. It will also fail if there are multiple inputs - which one should autofocus itself on rerender then?

    The issue happens when you define a component (FormComponent) inside the scope of another function which is called each render of your App component. This gives you a completely new FormComponent each time your App component is rerendered and calls useState. That new component is then, well, without focus.

    Personally I would feel against returning components from a hook. I would instead define a FormComponent component, and only return state from useForm state.

    But, a working example closest to your original code could be:

    // useForm.js
    import React, { useState } from "react";
    
    // Define the FormComponent outside of your useForm hook
    const FormComponent = ({ setState, state, label }) => (
      <form>
        <label htmlFor={label}>
          {label}
          <input
            type="text"
            id={label}
            value={state}
            placeholder={label}
            onChange={e => setState(e.target.value)}
          />
        </label>
      </form>
    );
    
    export default function useForm(defaultState, label) {
      const [state, setState] = useState(defaultState);
    
      return [
        state,
        <FormComponent state={state} setState={setState} label={label} />,
        setState
      ];
    }
    
    // App.js
    import useForm from "./useForm";
    
    export default function App() {
      const [formValue, Form] = useForm("San Francisco, CA", "Location");
    
      return (
        <>
          <h1>{formValue}</h1>
          {Form}
        </>
      );
    }
    

    Here's a sandbox