Как получить доступ к состоянию ребенка в React?

215

У меня есть следующая структура:

FormEditor- содержит несколько FieldEditor FieldEditor- редактирует поле формы и сохраняет различные значения об этом в своем состоянии

Когда кнопка нажата в FormEditor, я хочу иметь возможность собирать информацию о полях из всех FieldEditorкомпонентов, информацию, которая находится в их состоянии, и иметь все это в FormEditor.

Я подумал о том, чтобы хранить информацию о полях за пределами FieldEditorроссийского состояния и FormEditorвместо этого переводить ее в состояние. Однако для этого потребуется FormEditorпрослушивать каждый из его FieldEditorкомпонентов по мере их изменения и сохранять свою информацию в своем состоянии.

Разве я не могу просто получить доступ к состоянию детей? Это идеально?

Моше Рева
источник
4
«Разве я не могу просто получить доступ к состоянию детей? Это идеально?» Нет. Государство является чем-то внутренним и не должно просачиваться наружу. Вы можете реализовать методы доступа для вашего компонента, но даже это не идеально.
Феликс Клинг
@FelixKling Тогда вы предполагаете, что идеальный способ общения ребенка с родителем - это только события?
Моше Рева
3
Да, события это один из способов. Или используйте поток данных в одном направлении, как продвигает Flux: facebook.github.io/flux
Феликс Клинг,
1
Если вы не собираетесь использовать FieldEditors отдельно, сохраните их состояние в FormEditorхорошем звуке. Если это так, ваши FieldEditorэкземпляры будут отображаться на основе propsпереданного их редактором форм, а не их state. Более сложным, но гибким способом было бы создать сериализатор, который проходит через любые дочерние элементы контейнера, находит все FormEditorэкземпляры среди них и сериализует их в объект JSON. Объект JSON может быть необязательно вложенным (более одного уровня) на основе уровней вложенности экземпляров в редакторе форм.
Мельо
4
Я думаю, что React Docs «Поднятие состояния вверх» , вероятно, самый «Reacty» способ сделать это
icc97

Ответы:

136

Если у вас уже есть обработчик onChange для отдельных FieldEditors, я не понимаю, почему вы не могли просто переместить состояние в компонент FormEditor и просто передать обратный вызов оттуда в FieldEditors, который обновит родительское состояние. Мне кажется, это более эффективный способ сделать это.

Возможно, что-то в этом роде:

const FieldEditor = ({ value, onChange, id }) => {
  const handleChange = event => {
    const text = event.target.value;
    onChange(id, text);
  };

  return (
    <div className="field-editor">
      <input onChange={handleChange} value={value} />
    </div>
  );
};

const FormEditor = props => {
  const [values, setValues] = useState({});
  const handleFieldChange = (fieldId, value) => {
    setValues({ ...values, [fieldId]: value });
  };

  const fields = props.fields.map(field => (
    <FieldEditor
      key={field}
      id={field}
      onChange={handleFieldChange}
      value={values[field]}
    />
  ));

  return (
    <div>
      {fields}
      <pre>{JSON.stringify(values, null, 2)}</pre>
    </div>
  );
};

// To add abillity to dynamically add/remove fields keep the list in state
const App = () => {
  const fields = ["field1", "field2", "anotherField"];

  return <FormEditor fields={fields} />;
};

Оригинал - версия с предварительным крючком:

Markus-IPSE
источник
2
Это полезно для onChange, но не так много для onSubmit.
190290000 Рубль Человек
1
@ 190290000RubleMan Я не уверен, что вы имеете в виду, но поскольку обработчики onChange для отдельных полей гарантируют, что у вас всегда есть полные данные формы, доступные в вашем состоянии в компоненте FormEditor, ваш onSubmit будет просто включать что-то подобное myAPI.SaveStuff(this.state);. Если вы укажете немного больше, может быть, я могу дать вам лучший ответ :)
Markus-ipse
Скажем, у меня есть форма с 3 полями различного типа, каждое из которых имеет свою валидацию. Я хотел бы проверить каждый из них перед отправкой. Если проверка не пройдена, каждое поле с ошибкой должно пересчитываться. Я не понял, как сделать это с вашим предложенным решением, но, возможно, есть способ!
190290000 Рубль Человек
1
@ 190290000RubleMan Похоже, другой случай, чем оригинальный вопрос. Откройте новый вопрос с более подробной информацией и, возможно, с примером кода, и я смогу посмотреть позже :)
Markus-ipse
Этот ответ может оказаться полезным : он использует хуки состояния, он охватывает, где должно создаваться состояние, как избежать повторного рендеринга формы при каждом нажатии клавиши, как выполнять проверку поля и т. Д.
Эндрю
189

Непосредственно перед тем, как я подробно расскажу о том, как вы можете получить доступ к состоянию дочернего компонента, пожалуйста, прочитайте ответ Markus-ipse относительно лучшего решения для этого конкретного сценария.

Если вы действительно хотите получить доступ к состоянию дочерних компонентов, вы можете назначить свойство, вызываемое refдля каждого дочернего элемента . Теперь есть два способа реализации ссылок: использование React.createRef()и обратный вызов refs.

С помощью React.createRef()

В настоящее время это рекомендуемый способ использования ссылок с React 16.3 (дополнительную информацию см. В документации ). Если вы используете более раннюю версию, то смотрите ниже ссылки обратного вызова.

Вам нужно будет создать новую ссылку в конструкторе вашего родительского компонента, а затем назначить ее дочернему элементу через refатрибут.

class FormEditor extends React.Component {
  constructor(props) {
    super(props);
    this.FieldEditor1 = React.createRef();
  }
  render() {
    return <FieldEditor ref={this.FieldEditor1} />;
  }
}

Чтобы получить доступ к этому виду ссылки, вам необходимо использовать:

const currentFieldEditor1 = this.FieldEditor1.current;

Это вернет экземпляр смонтированного компонента, так что вы сможете использовать его currentFieldEditor1.stateдля доступа к состоянию.

Просто небольшое замечание, чтобы сказать, что если вы используете эти ссылки на узле DOM вместо компонента (например <div ref={this.divRef} />), то this.divRef.currentвернет базовый элемент DOM вместо экземпляра компонента.

Обратный звонок

Это свойство принимает функцию обратного вызова, которой передается ссылка на присоединенный компонент. Этот обратный вызов выполняется сразу после монтирования или демонтажа компонента.

Например:

<FieldEditor
    ref={(fieldEditor1) => {this.fieldEditor1 = fieldEditor1;}
    {...props}
/>

В этих примерах ссылка хранится на родительском компоненте. Чтобы вызвать этот компонент в своем коде, вы можете использовать:

this.fieldEditor1

а затем использовать, this.fieldEditor1.stateчтобы получить состояние.

Прежде всего, убедитесь, что ваш дочерний компонент отрисован, прежде чем пытаться получить к нему доступ ^ _ ^

Как и выше, если вы используете эти ссылки на узле DOM вместо компонента (например <div ref={(divRef) => {this.myDiv = divRef;}} />), тогда this.divRefбудет возвращаться базовый элемент DOM вместо экземпляра компонента.

Дальнейшая информация

Если вы хотите узнать больше о недвижимости React, проверьте эту страницу на Facebook.

Обязательно прочитайте раздел « Не злоупотребляйте ссылками », в котором говорится, что вы не должны использовать ребенка, stateчтобы «добиться успеха».

Надеюсь, это поможет ^ _ ^

Редактировать: Добавлен React.createRef()метод для создания ссылок. Удален код ES5.

Мартоид Прайм
источник
5
Также аккуратный маленький кусочек, вы можете вызывать функции из this.refs.yourComponentNameHere. Я нашел это полезным для изменения состояния с помощью функций. Пример: this.refs.textInputField.clearInput();
Дилан Пирс
7
Осторожно (из документов): ссылки - это отличный способ отправить сообщение определенному дочернему экземпляру способом, который было бы неудобно делать с помощью потоковой передачи Reactive propsи state. Однако они не должны быть вашей основной абстракцией для передачи данных через ваше приложение. По умолчанию используйте Поток данных Reactive и сохраняйте refs для вариантов использования, которые по своей природе не являются реактивными.
Линн
2
Я получаю только то состояние, которое было определено внутри конструктора (не обновленное состояние)
Владо Панджич
3
Использование ссылок теперь классифицируется как использование « Неконтролируемых компонентов » с рекомендацией использовать «Контролируемые компоненты»
icc97
Возможно ли это сделать, если дочерний компонент является connectedкомпонентом Redux ?
jmancherje
7

Его 2020 и многие из вас приедут сюда в поисках аналогичного решения, но с Хуками (они великолепны!) И с новейшими подходами с точки зрения чистоты кода и синтаксиса.

Таким образом, как указывалось в предыдущих ответах, наилучший подход к решению этой проблемы состоит в том, чтобы удерживать состояние вне дочернего компонента fieldEditor. Вы можете сделать это несколькими способами.

Наиболее «сложным» является глобальный контекст (состояние), к которому могут обращаться и изменять как родитель, так и потомки. Это отличное решение, когда компоненты очень глубоко в древовидной иерархии, и поэтому дорого отправлять реквизиты на каждом уровне.

В этом случае я думаю, что это того не стоит, и более простой подход принесет нам результаты, которых мы хотим, просто используя мощные React.useState().

Подход с хуком React.useState (), намного проще, чем с компонентами класса

Как уже было сказано, мы будем иметь дело с изменениями и будем хранить данные нашего дочернего компонента fieldEditorв нашем родителе fieldForm. Для этого мы отправим ссылку на функцию, которая будет обрабатывать и применять изменения к fieldFormсостоянию, вы можете сделать это с помощью:

function FieldForm({ fields }) {
  const [fieldsValues, setFieldsValues] = React.useState({});
  const handleChange = (event, fieldId) => {
    let newFields = { ...fieldsValues };
    newFields[fieldId] = event.target.value;

    setFieldsValues(newFields);
  };

  return (
    <div>
      {fields.map(field => (
        <FieldEditor
          key={field}
          id={field}
          handleChange={handleChange}
          value={fieldsValues[field]}
        />
      ))}
      <div>{JSON.stringify(fieldsValues)}</div>
    </div>
  );
}

Обратите внимание, что React.useState({})будет возвращен массив с позицией 0, являющейся значением, указанным при вызове (в данном случае пустым объектом), а позицией 1 будет ссылка на функцию, которая изменяет значение.

Теперь, с дочерним компонентом, FieldEditorвам даже не нужно создавать функцию с оператором return, подойдет константа lean с функцией стрелки!

const FieldEditor = ({ id, value, handleChange }) => (
  <div className="field-editor">
    <input onChange={event => handleChange(event, id)} value={value} />
  </div>
);

Аааа, и все готово, не более того, только с этими двумя функциональными компонентами слизи у нас есть конечная цель - получить доступ к нашему дочернему FieldEditorзначению и показать его в нашем родителе.

Вы можете проверить принятый ответ 5 лет назад и посмотреть, как Hooks сделал код React более простым (на много!).

Надеюсь, мой ответ поможет вам узнать и понять больше о крючках, и если вы хотите проверить рабочий пример, вот он .

Хосеп Видаль
источник
4

Теперь вы можете получить доступ к состоянию InputField, которое является дочерним по отношению к FormEditor.

По сути, всякий раз, когда происходит изменение в состоянии поля ввода (дочернего), мы получаем значение из объекта события и затем передаем это значение в Parent, где устанавливается состояние в Parent.

При нажатии кнопки мы просто печатаем состояние полей ввода.

Ключевым моментом здесь является то, что мы используем реквизиты для получения идентификатора / значения поля ввода, а также для вызова функций, которые задаются как атрибуты поля ввода, в то время как мы генерируем многократно используемые дочерние поля ввода.

class InputField extends React.Component{
  handleChange = (event)=> {
    const val = event.target.value;
    this.props.onChange(this.props.id , val);
  }

  render() {
    return(
      <div>
        <input type="text" onChange={this.handleChange} value={this.props.value}/>
        <br/><br/>
      </div>
    );
  }
}       


class FormEditorParent extends React.Component {
  state = {};
  handleFieldChange = (inputFieldId , inputFieldValue) => {
    this.setState({[inputFieldId]:inputFieldValue});
  }
  //on Button click simply get the state of the input field
  handleClick = ()=>{
    console.log(JSON.stringify(this.state));
  }

  render() {
    const fields = this.props.fields.map(field => (
      <InputField
        key={field}
        id={field}
        onChange={this.handleFieldChange}
        value={this.state[field]}
      />
    ));

    return (
      <div>
        <div>
          <button onClick={this.handleClick}>Click Me</button>
        </div>
        <div>
          {fields}
        </div>
      </div>
    );
  }
}

const App = () => {
  const fields = ["field1", "field2", "anotherField"];
  return <FormEditorParent fields={fields} />;
};

ReactDOM.render(<App/>, mountNode);
Dhana
источник
7
Не уверен, что смогу поддержать это. Что вы упоминаете здесь, на который еще не ответил @ Markus-ipse?
Александр Берд
4

Как и в предыдущих ответах, попробуйте переместить состояние в верхний компонент и изменить его с помощью обратных вызовов, передаваемых его дочерним элементам.

В случае, если вам действительно нужен доступ к дочернему состоянию, которое объявлено как функциональный компонент (хуки), вы можете объявить ref в родительском компоненте, а затем передать его как атрибут ref дочернему элементу, но вам нужно использовать React.forwardRef. а затем хук useImperativeHandle для объявления функции, которую вы можете вызвать в родительском компоненте.

Взгляните на следующий пример:

const Parent = () => {
    const myRef = useRef();
    return <Child ref={myRef} />;
}

const Child = React.forwardRef((props, ref) => {
    const [myState, setMyState] = useState('This is my state!');
    useImperativeHandle(ref, () => ({getMyState: () => {return myState}}), [myState]);
})

Тогда вы сможете получить myState в родительском компоненте, вызвав: myRef.current.getMyState();

Маурисио Авенданьо
источник