Jak uzyskać dostęp do stanu dziecka w React?

214

Mam następującą strukturę:

FormEditor- posiada wiele FieldEditor FieldEditor- edytuje pole formularza i zapisuje różne wartości na jego temat w jego stanie

Po kliknięciu przycisku w formularzu FormEditor chcę być w stanie zebrać informacje o polach ze wszystkich FieldEditorskładników, informacje w ich stanie i mieć je wszystkie w formularzu FormEditor.

Rozważyłem przechowywanie informacji o polach poza FieldEditorstanem i FormEditorzamiast tego umieściłem je w stanie. Wymagałoby FormEditorto jednak wysłuchania każdego z jego FieldEditorskładników, gdy zmieniają się i przechowują informacje w tym stanie.

Czy nie mogę po prostu uzyskać dostępu do stanu dzieci? Czy to jest idealne?

Moshe Revah
źródło
4
„Czy nie mogę po prostu uzyskać dostępu do stanu dzieci? Czy to jest idealne?” Nie. Stan jest czymś wewnętrznym i nie powinien przeciekać na zewnątrz. Możesz zaimplementować metody akcesyjne dla swojego komponentu, ale nawet to nie jest idealne.
Felix Kling
@FelixKling Sugerujesz, że idealnym sposobem na komunikację dziecka z rodzicem są tylko zdarzenia?
Moshe Revah,
3
Tak, wydarzenia to jeden sposób. Lub poproś o przepływ danych w jednym kierunku, taki jak promowany przez Flux: facebook.github.io/flux
Felix Kling
1
Jeśli nie zamierzasz używać FieldEditors osobno, zapisywanie ich stanu w FormEditordobrym brzmieniu. W takim przypadku FieldEditorinstancje będą renderowane na podstawie propsprzekazanych przez ich edytor formularzy, a nie ich state. Bardziej złożonym, ale elastycznym sposobem byłoby utworzenie serializatora, który przechodzi przez dowolne elementy potomne kontenera i znajduje wszystkie FormEditorwystąpienia między nimi i serializuje je do obiektu JSON. Obiekt JSON może być opcjonalnie zagnieżdżony (więcej niż jeden poziom) na podstawie poziomów zagnieżdżenia instancji w edytorze formularzy.
Meglio
4
Myślę, że React Docs „Lifting State Up” jest prawdopodobnie najbardziej „Reacty” sposobem na zrobienie tego
icc97

Odpowiedzi:

135

Jeśli masz już moduł obsługi onChange dla poszczególnych FieldEditors, nie rozumiem, dlaczego nie możesz po prostu przenieść stanu do komponentu FormEditor i po prostu przekazać stamtąd wywołanie zwrotne do FieldEditors, które zaktualizują stan nadrzędny. Dla mnie to wydaje się bardziej reaktywnym sposobem.

Być może coś w tym rodzaju:

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} />;
};

Oryginał - wersja z zaczepami:

Markus-ipse
źródło
2
Jest to przydatne w przypadku onChange, ale nie tak bardzo w onSubmit.
190290000 Rubel Man
1
@ 190290000RubleMan Nie jestem pewien, co masz na myśli, ale ponieważ programy obsługi onChange w poszczególnych polach zapewniają, że zawsze masz pełne dane formularza w swoim stanie w komponencie FormEditor, twój onSubmit po prostu zawiera coś takiego myAPI.SaveStuff(this.state);. Jeśli opracujesz trochę więcej, może dam ci lepszą odpowiedź :)
Markus-ipse
Powiedzmy, że mam formularz z 3 polami różnego typu, każde z własną weryfikacją. Chciałbym zweryfikować każdą z nich przed przesłaniem. Jeśli sprawdzanie poprawności się nie powiedzie, każde pole z błędem powinno zostać ponownie wysłane. Nie wymyśliłem, jak to zrobić za pomocą proponowanego rozwiązania, ale być może istnieje sposób!
190290000 Rubel Man
1
@ 190290000RubleMan Wydaje się, że jest to inna sprawa niż oryginalne pytanie. Otwórz nowe pytanie, podając więcej szczegółów i być może jakiś przykładowy kod, i mogę zajrzeć później :)
Markus-ipse
Ta odpowiedź może być przydatna : wykorzystuje haczyki stanu, obejmuje to, gdzie należy utworzyć stan, jak uniknąć ponownego renderowania formularza przy każdym naciśnięciu klawisza, jak sprawdzać poprawność pola itp.
Andrew
189

Zanim przejdę do szczegółów na temat uzyskiwania dostępu do stanu elementu potomnego, przeczytaj odpowiedź Markusa-ipse dotyczącą lepszego rozwiązania tego konkretnego scenariusza.

Jeśli rzeczywiście chcesz uzyskać dostęp do stanu elementów potomnych komponentu, możesz przypisać właściwość wywoływaną refdla każdego elementu potomnego. Istnieją teraz dwa sposoby implementacji referencji: Korzystanie z referencji React.createRef()i wywołanie zwrotne.

Za pomocą React.createRef()

Jest to obecnie zalecany sposób używania referencji od React 16.3 ( Więcej informacji można znaleźć w dokumentacji ). Jeśli używasz wcześniejszej wersji, zapoznaj się z poniższymi informacjami na temat odwołań zwrotnych.

Musisz utworzyć nowe odwołanie w konstruktorze komponentu nadrzędnego, a następnie przypisać je do elementu podrzędnego za pomocą refatrybutu.

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

Aby uzyskać dostęp do tego rodzaju referencji, musisz użyć:

const currentFieldEditor1 = this.FieldEditor1.current;

Zwróci to instancję zamontowanego komponentu, abyś mógł następnie użyć, currentFieldEditor1.stateaby uzyskać dostęp do stanu.

Krótka uwaga, aby powiedzieć, że jeśli użyjesz tych odniesień w węźle DOM zamiast w komponencie (np. <div ref={this.divRef} />), Wówczas this.divRef.currentzwróci podstawowy element DOM zamiast instancji komponentu.

Oddzwanianie

Ta właściwość przyjmuje funkcję wywołania zwrotnego, która jest przekazywana do dołączonego komponentu. To wywołanie zwrotne jest wykonywane natychmiast po zamontowaniu lub odmontowaniu komponentu.

Na przykład:

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

W tych przykładach odwołanie jest przechowywane w komponencie nadrzędnym. Aby wywołać ten składnik w kodzie, możesz użyć:

this.fieldEditor1

a następnie użyj, this.fieldEditor1.stateaby uzyskać stan.

Należy zwrócić uwagę na jedną rzecz: upewnij się, że komponent potomny jest renderowany, zanim spróbujesz uzyskać do niego dostęp ^ ^ ^

Jak wyżej, jeśli użyjesz tych odniesień w węźle DOM zamiast w komponencie (np. <div ref={(divRef) => {this.myDiv = divRef;}} />), Wówczas this.divRefzwróci podstawowy element DOM zamiast instancji komponentu.

Dalsza informacja

Jeśli chcesz dowiedzieć się więcej o nieruchomości referencyjnej React, sprawdź tę stronę na Facebooku.

Upewnij się, że przeczytałeś sekcję „ Nie nadużywaj referencji ”, która mówi, że nie powinieneś używać dziecka statedo „sprawiania, by coś się działo”.

Mam nadzieję, że to pomoże ^ _ ^

Edycja: Dodano React.createRef()metodę tworzenia referencji. Usunięto kod ES5.

Martoid Prime
źródło
5
Również schludny mały smakołyk, z którego można wywoływać funkcje this.refs.yourComponentNameHere. Uznałem, że jest to przydatne do zmiany stanu za pomocą funkcji. Przykład: this.refs.textInputField.clearInput();
Dylan Pierce,
7
Uwaga (z dokumentów): Odnośniki to świetny sposób na wysłanie wiadomości do określonej instancji podrzędnej w sposób, który byłby niewygodny za pośrednictwem przesyłania strumieniowego Reactive propsi state. Nie powinny one jednak służyć jako abstrakcja dla przepływających danych przez aplikację. Domyślnie użyj Reaktywnego przepływu danych i zapisz refs dla przypadków użycia, które z natury nie są reaktywne.
Lynn,
2
Dostaję tylko stan, który został zdefiniowany w konstruktorze (stan nieaktualny)
Vlado Pandžić
3
Używanie referencji jest teraz klasyfikowane jako używanie „ niekontrolowanych komponentów ” z zaleceniem używania „kontrolowanych komponentów”
icc97
Czy można to zrobić, jeśli element potomny jest connectedskładnikiem Redux ?
jmancherje
7

Jest rok 2020 i wielu z was przyjedzie tutaj, szukając podobnego rozwiązania, ale z Hakami (są świetne!) I najnowszymi podejściami pod względem czystości kodu i składni.

Tak więc, jak stwierdzono w poprzednich odpowiedziach, najlepszym podejściem do tego rodzaju problemu jest utrzymywanie stanu poza komponentem potomnym fieldEditor. Możesz to zrobić na wiele sposobów.

Najbardziej „skomplikowany” jest kontekst globalny (stan), do którego zarówno rodzic, jak i dziecko mogą uzyskać dostęp i modyfikować. To świetne rozwiązanie, gdy komponenty znajdują się bardzo głęboko w hierarchii drzew, a zatem kosztowne jest wysyłanie rekwizytów na każdym poziomie.

W tym przypadku uważam, że nie jest tego warte, a prostsze podejście przyniesie nam pożądane wyniki, używając tylko potężnego React.useState().

Podejdź do haka React.useState (), znacznie prościej niż przy komponentach Class

Jak już powiedzieliśmy, zajmiemy się zmianami i będziemy przechowywać dane naszego elementu potomnego fieldEditorw naszym rodzicu fieldForm. Aby to zrobić, wyślemy referencję do funkcji, która zajmie się zmianami fieldFormstanu i zastosuje je , możesz to zrobić za pomocą:

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>
  );
}

Zauważ, że React.useState({})zwróci tablicę z pozycją 0 będącą wartością określoną podczas wywołania (w tym przypadku pusty obiekt), a pozycja 1 będzie odniesieniem do funkcji, która modyfikuje wartość.

Teraz dzięki komponentowi FieldEditorpotomnemu nie musisz nawet tworzyć funkcji z instrukcją return, wystarczy stała stała z funkcją strzałki!

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

Aaaaaaaaaaaaaaaaaaaaaa, i nic więcej, tylko te dwa śluzowe elementy funkcjonalne mają nasz cel końcowy „dostęp” do naszej FieldEditorwartości dla dziecka i pokazanie jej u naszego rodzica.

Możesz sprawdzić zaakceptowaną odpowiedź sprzed 5 lat i zobaczyć, w jaki sposób Hooks sprawił, że kod React był węższy (o wiele!).

Mam nadzieję, że moja odpowiedź pomoże Ci dowiedzieć się i zrozumieć więcej o Hakach, a jeśli chcesz sprawdzić działający przykład, oto ona .

Josep Vidal
źródło
4

Teraz możesz uzyskać dostęp do stanu InputField, który jest potomkiem FormEditor.

Zasadniczo za każdym razem, gdy następuje zmiana stanu pola wejściowego (dziecko), otrzymujemy wartość z obiektu zdarzenia, a następnie przekazujemy tę wartość do obiektu nadrzędnego, w którym ustawiony jest stan w obiekcie nadrzędnym.

Po kliknięciu przycisku drukujemy tylko stan pól wejściowych.

Kluczową kwestią jest to, że używamy rekwizytów, aby uzyskać identyfikator / wartość pola wejściowego, a także wywołać funkcje ustawione jako atrybuty w polu wejściowym podczas generowania potomnych pól wejściowych wielokrotnego użytku.

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
źródło
7
Nie jestem pewien, czy mogę głosować za tym. O czym tu wspominasz, na co @ Markus-ipse jeszcze nie odpowiedział?
Alexander Bird
3

Jak powiedziano w poprzednich odpowiedziach, spróbuj przenieść stan do najwyższego komponentu i zmodyfikuj go przez wywołania zwrotne przekazywane do jego dzieci.

Jeśli naprawdę potrzebujesz dostępu do stanu potomnego zadeklarowanego jako składnik funkcjonalny (przechwytuje), możesz zadeklarować ref w komponencie nadrzędnym, a następnie przekazać go jako atrybut ref do dziecka, ale musisz użyć React.forwardRef a następnie hook użyjImperativeHandle, aby zadeklarować funkcję, którą możesz wywołać w komponencie nadrzędnym.

Spójrz na następujący przykład:

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]);
})

Następnie powinieneś być w stanie uzyskać myState w komponencie Parent, wywołując: myRef.current.getMyState();

Mauricio Avendaño
źródło