Tengo un formulario con varias capas de componentes secundarios. El estado del formulario se mantiene al más alto nivel y paso funciones como accesorios para actualizar el nivel superior. El único problema con esto es cuando el formulario se hace muy grande (puede agregar preguntas dinámicamente) cada componente se recarga cuando uno de ellos se actualiza. Aquí hay una versión simplificada de mi código (o el codesandbox: https://codesandbox.io/s/636xwz3rr):

const App = () => {
  return <Form />;
}

const initialForm = {
  id: 1,
  sections: [
    {
      ordinal: 1,
      name: "Section Number One",
      questions: [
        { ordinal: 1, text: "Who?", response: "" },
        { ordinal: 2, text: "What?", response: "" },
        { ordinal: 3, text: "Where?", response: "" }
      ]
    },
    {
      ordinal: 2,
      name: "Numero dos",
      questions: [
        { ordinal: 1, text: "Who?", response: "" },
        { ordinal: 2, text: "What?", response: "" },
        { ordinal: 3, text: "Where?", response: "" }
      ]
    }
  ]
};

const Form = () => {
  const [form, setForm] = useState(initialForm);

  const updateSection = (idx, value) => {
    const { sections } = form;
    sections[idx] = value;
    setForm({ ...form, sections });
  };

  return (
    <>
      {form.sections.map((section, idx) => (
        <Section
          key={section.ordinal}
          section={section}
          updateSection={value => updateSection(idx, value)}
        />
      ))}
    </>
  );
};

const Section = props => {
  const { section, updateSection } = props;

  const updateQuestion = (idx, value) => {
    const { questions } = section;
    questions[idx] = value;
    updateSection({ ...section, questions });
  };

  console.log(`Rendered section "${section.name}"`);

  return (
    <>
      <div style={{ fontSize: 18, fontWeight: "bold", margin: "24px 0" }}>
        Section name:
        <input
          type="text"
          value={section.name}
          onChange={e => updateSection({ ...section, name: e.target.value })}
        />
      </div>
      <div style={{ marginLeft: 36 }}>
        {section.questions.map((question, idx) => (
          <Question
            key={question.ordinal}
            question={question}
            updateQuestion={v => updateQuestion(idx, v)}
          />
        ))}
      </div>
    </>
  );
};

const Question = props => {
  const { question, updateQuestion } = props;

  console.log(`Rendered question #${question.ordinal}`);

  return (
    <>
      <div>{question.text}</div>
      <input
        type="text"
        value={question.response}
        onChange={e =>
          updateQuestion({ ...question, response: e.target.value })
        }
      />
    </>
  );
};

Intenté usar useMemo y useCallback, pero no puedo entender cómo hacerlo funcionar. El problema es transmitir la función para actualizar su padre. No puedo entender cómo hacerlo sin actualizarlo cada vez que se actualiza el formulario.

No puedo encontrar una solución en línea en ningún lado. Tal vez estoy buscando lo incorrecto. ¡Gracias por cualquier ayuda que pueda ofrecer!

Solución

Usando la respuesta de Andrii-Golubenko y este artículo Reaccione optimizaciones con React.memo, useCallback y useReducer pude encontrar esta solución: https://codesandbox.io/s/myrjqrjm18

Observe cómo el registro de la consola solo muestra la representación de los componentes que han cambiado.

8
Michael Cox 10 may. 2019 a las 22:05

3 respuestas

La mejor respuesta
  1. Use la función React React.memo para componentes funcionales para evitar volver a renderizar si los accesorios no cambian, de manera similar a PureComponent para componentes de clase.
  2. Cuando pasa la devolución de llamada de esa manera:
<Section
    ...
    updateSection={value => updateSection(idx, value)}
/>

Su componente Section se volverá a mostrar cada vez que se vuelva a generar el componente principal, incluso si no se cambian otros accesorios y se usa React.memo. Porque su devolución de llamada se volverá a crear cada vez que se represente el componente principal. Debe envolver su devolución de llamada en gancho useCallback.

  1. Usar useState no es una buena decisión si necesita almacenar objetos complejos como initialForm. Es mejor usar useReducer;

Aquí puede ver la solución de trabajo: https://codesandbox.io/s/o10p05m2vz

5
Andrii Golubenko 12 feb. 2020 a las 15:56

Sugeriría usar métodos de ciclo de vida para evitar la reproducción, en el ejemplo de ganchos de reacción que puede usar, useEffect. También centralizar su estado en contexto y usar el enlace useContext probablemente también ayudaría.

0
Brad Ball 10 may. 2019 a las 20:24

Trabajando a través de este problema con un formulario complejo, el truco que implementé fue utilizar onBlur={updateFormState} en los elementos de entrada del componente para activar la elevación de los datos del formulario al componente principal a través de una función pasada como accesorio al componente.

Para actualizar el elemento de entrada del componente, usé onChange={handleInput} usando un estado dentro del componente, qué estado del componente se pasó a la función de elevación cuando la entrada (o el componente en su conjunto, si hay múltiples campos de entrada en el componente ) perdió el enfoque.

Esto es un poco hacky, y probablemente esté mal por alguna razón, pero funciona en mi máquina. ;-)

0
Charles Goodwin 7 feb. 2020 a las 21:09