Jaki jest właściwy sposób przekazania stanu elementu formularza do elementów rodzeństwa / rodzica?

186
  • Załóżmy, że mam klasę React P, która renderuje dwie klasy potomne, C1 i C2.
  • C1 zawiera pole wprowadzania. Odniosę się do tego pola wejściowego jako Foo.
  • Moim celem jest pozwolić C2 reagować na zmiany w Foo.

Wymyśliłem dwa rozwiązania, ale żadne z nich nie jest w porządku.

Pierwsze rozwiązanie:

  1. Przypisywanie P państwem, state.input.
  2. Utwórz onChangefunkcję w P, która przyjmuje zdarzenie i ustawia state.input.
  3. Przekaż to onChangeC1 jako a propsi pozwól C1 związać this.props.onChangesię onChangez Foo.

To działa. Ilekroć zmienia się wartość Foo, wyzwala a setStatew P, więc P będzie miał dane wejściowe do przekazania do C2.

Ale to nie wydaje się właściwe z tego samego powodu: ustawiam stan elementu nadrzędnego na podstawie elementu podrzędnego. Wydaje się, że zdradza to zasadę projektowania React: przepływ danych w jednym kierunku.
Czy tak powinienem to zrobić, czy może istnieje bardziej naturalne rozwiązanie React?

Drugie rozwiązanie:

Po prostu umieść Foo w P.

Ale czy to zasada projektowania, którą powinienem stosować, tworząc strukturę mojej aplikacji - umieszczając wszystkie elementy formularza w renderklasie najwyższego poziomu?

Podobnie jak w moim przykładzie, jeśli mam duży rendering C1, naprawdę nie chcę umieszczać całego renderC1 na renderP tylko dlatego, że C1 ma element form.

Jak mam to zrobić?

oktref
źródło
Zaraz zrobię dokładnie to samo i pomimo tego, że działa poprawnie, mam wrażenie, że to tylko gigantyczny hack
Mirko

Odpowiedzi:

198

Więc jeśli dobrze cię rozumiem, twoje pierwsze rozwiązanie sugeruje, że utrzymujesz stan w swoim głównym komponencie? Nie mogę mówić za twórcami React, ale ogólnie uważam, że jest to właściwe rozwiązanie.

Utrzymanie stanu jest jednym z powodów (przynajmniej tak mi się wydaje), że React został stworzony. Jeśli kiedykolwiek wdrożyłeś własną stronę klienta ze wzorcem stanu do radzenia sobie z dynamicznym interfejsem użytkownika, który ma wiele wzajemnie zależnych elementów ruchomych, pokochasz React, ponieważ łagodzi on wiele bólu zarządzania stanem.

Utrzymując stan na wyższym poziomie w hierarchii i aktualizując go za pomocą zdarzeń, przepływ danych jest nadal prawie jednokierunkowy, po prostu reagujesz na zdarzenia w komponencie Root, tak naprawdę nie dostajesz tam danych przez wiązanie dwukierunkowe, mówisz komponentowi Root, że „hej, coś się tutaj wydarzyło, sprawdź wartości” lub przekazujesz stan niektórych danych w komponencie potomnym w celu aktualizacji stanu. Zmieniłeś stan w C1 i chcesz, aby C2 był tego świadomy, więc aktualizując stan w komponencie głównym i ponownie renderując, rekwizyty C2 są teraz zsynchronizowane, ponieważ stan został zaktualizowany w komponencie głównym i przekazany dalej .

class Example extends React.Component {
  constructor (props) {
    super(props)
    this.state = { data: 'test' }
  }
  render () {
    return (
      <div>
        <C1 onUpdate={this.onUpdate.bind(this)}/>
        <C2 data={this.state.data}/>
      </div>
    )
  }
  onUpdate (data) { this.setState({ data }) }
}

class C1 extends React.Component {
    render () {
      return (
        <div>
          <input type='text' ref='myInput'/>
          <input type='button' onClick={this.update.bind(this)} value='Update C2'/>
        </div>
      )
    }
    update () {
      this.props.onUpdate(this.refs.myInput.getDOMNode().value)
    }
})

class C2 extends React.Component {
    render () {
      return <div>{this.props.data}</div>
    }
})

ReactDOM.renderComponent(<Example/>, document.body)
captray
źródło
5
Nie ma problemu. W rzeczywistości wróciłem po napisaniu tego postu i ponownie przeczytałem część dokumentacji i wydaje się, że jest to zgodne z ich myśleniem i najlepszymi praktykami. React ma naprawdę świetną dokumentację i za każdym razem, gdy zastanawiam się, gdzie coś powinno pójść, zazwyczaj mają to gdzieś w dokumentacji. Sprawdź sekcję dotyczącą stanu tutaj, facebook.github.io/react/docs/…
captray
@captray, ale co powiesz na to, jeśli C2masz getInitialStatedane i wewnątrz których renderużywa this.state.data?
Dmitry Polushkin
2
@DmitryPolushkin Jeśli dobrze rozumiem twoje pytanie, chcesz przekazać dane z głównego komponentu do C2 jako rekwizyty. W C2 te dane byłyby ustawione jako stan początkowy (tj. GetInitialState: function () {return {someData: this.props.dataFromParentThatWillChange}} i będziesz chciał zaimplementować componentWillReceiveProps i wywołać this.setState z nowymi rekwizytami, aby zaktualizować stan w C2. Odkąd początkowo odpowiedziałem, używam Fluxa, i zdecydowanie polecam, abyś też na to spojrzał.
Uczyni
3
@DmitryPolushkin Chciałem opublikować to w dalszym ciągu. facebook.github.io/react/tips/… Wszystko jest w porządku, o ile wiesz, co robisz i wiesz, że dane się zmienią, ale w wielu sytuacjach prawdopodobnie możesz to zmieniać. Ważne jest również, aby pamiętać, że nie trzeba budować tego jako hierarchii. Możesz zamontować C1 i C2 w różnych miejscach w DOM, a oni mogą nasłuchiwać zmian zdarzeń w niektórych danych. Widzę, że wiele osób naciska na elementy hierarchiczne, gdy nie są one potrzebne.
captray
5
Powyższy kod zawiera 2 błędy - oba wiążą się z niepowiązaniem „tego” we właściwym kontekście, dokonałem powyższych poprawek, a także dla każdego, kto potrzebuje demonstracji codepen
Alex H
34

Po użyciu aplikacji React do zbudowania aplikacji chciałbym podzielić się przemyśleniami na temat pytania, które zadałem pół roku temu.

Polecam przeczytać

Pierwszy post jest niezwykle pomocny w zrozumieniu, w jaki sposób należy uporządkować aplikację React.

Flux odpowiada na pytanie, dlaczego powinieneś tak ustrukturyzować aplikację React (a nie jak to zrobić) ją ustrukturyzować). React to tylko 50% systemu, a dzięki Flux możesz zobaczyć cały obraz i zobaczyć, jak tworzą one spójny system.

Powrót do pytania

Jeśli chodzi o moje pierwsze rozwiązanie, to jest całkowicie OK, aby pozwolić handlerowi iść w przeciwnym kierunku, ponieważ dane wciąż idą w jednym kierunku.

Jednak to, czy zezwolenie funkcji obsługi na wywołanie setState w P może być prawidłowe, czy niepoprawne, w zależności od sytuacji.

Jeśli aplikacja jest prostym konwerterem Markdown, C1 jest surowym wejściem, a C2 wyjściem HTML, możesz pozwolić C1 wyzwalać setState w P, ale niektórzy mogą argumentować, że nie jest to zalecany sposób.

Jeśli jednak aplikacja jest listą czynności do wykonania, C1 stanowi dane wejściowe do utworzenia nowej czynności do wykonania, C2 jest to lista czynności do wykonania w HTML, prawdopodobnie prawdopodobnie chcesz, aby program obsługiwał przejście o dwa poziomy wyżej niż P - do dispatcher, co pozwala na storeaktualizację data store, które następnie przesyłają dane do P i wypełniają widoki. Zobacz ten artykuł Flux. Oto przykład: Flux - TodoMVC

Ogólnie wolę sposób opisany w przykładzie z listą rzeczy do zrobienia. Im mniej masz stanu w swojej aplikacji, tym lepiej.

oktref
źródło
7
Często omawiam to w prezentacjach dotyczących zarówno React, jak i Flux. Punkt, który próbuję podkreślić, jest tym, co opisałeś powyżej, a mianowicie oddzieleniem stanu widoku od stanu aplikacji. Są sytuacje, w których rzeczy mogą przejść z bycia stanem widoku do stanu aplikacji, szczególnie w sytuacjach, w których zapisujesz stan interfejsu użytkownika (np. Ustawione wartości). Myślę, że to niesamowite, że wróciłeś ze swoimi myślami rok później. +1
captray
@captray Czy możemy więc powiedzieć, że redux jest potężny niż reagować w każdych okolicznościach? Pomimo krzywej uczenia się ... (pochodzącej z Javy)
Bruce Sun
Nie jestem pewny co masz na myśli. Bardzo dobrze radzą sobie z dwiema różnymi rzeczami i stanowią właściwy rozdział obaw.
captray
1
Mamy projekt wykorzystujący redux, a miesiące temu wydaje się, że do wszystkiego służą akcje. To jak ta wielka mieszanka kodu spaghetti i niemożliwa do zrozumienia, totalna katastrofa. Zarządzanie stanem może być dobre, ale być może bardzo źle zrozumiane. Czy można bezpiecznie założyć, że flux / redux powinien być używany tylko w częściach stanu, do których należy uzyskać dostęp globalnie?
wayofthefureure
1
@wayofthefuture Nie widząc twojego kodu, mogę powiedzieć, że widzę dużo spaghetti Reaguj tak jak dobre ole spaghetti jQuery. Najlepsza rada, jaką mogę zaoferować, to postąpić zgodnie z SRP. Uczyń komponenty tak prostymi, jak to możliwe; głupie elementy renderujące, jeśli możesz. Naciskam również na abstrakty, takie jak komponent <DataProvider />. Robi jedną rzecz dobrze. Podaj dane. Zwykle staje się to komponentem „root” i przekazuje dane poprzez wykorzystywanie dzieci (i klonowanie) jako rekwizytów (zdefiniowana umowa). Ostatecznie spróbuj najpierw pomyśleć o serwerze. Sprawi, że JS będzie znacznie czystszy dzięki lepszym strukturom danych.
captray
6

Pięć lat później, wraz z wprowadzeniem React Hooks, istnieje teraz o wiele bardziej elegancki sposób korzystania z haka useContext.

Kontekst definiujesz w zasięgu globalnym, eksportujesz zmienne, obiekty i funkcje w komponencie nadrzędnym, a następnie zawijasz elementy podrzędne w aplikacji w podanym kontekście i importujesz wszystko, czego potrzebujesz w komponentach podrzędnych. Poniżej znajduje się dowód koncepcji.

import React, { useState, useContext } from "react";
import ReactDOM from "react-dom";
import styles from "./styles.css";

// Create context container in a global scope so it can be visible by every component
const ContextContainer = React.createContext(null);

const initialAppState = {
  selected: "Nothing"
};

function App() {
  // The app has a state variable and update handler
  const [appState, updateAppState] = useState(initialAppState);

  return (
    <div>
      <h1>Passing state between components</h1>

      {/* 
          This is a context provider. We wrap in it any children that might want to access
          App's variables.
          In 'value' you can pass as many objects, functions as you want. 
           We wanna share appState and its handler with child components,           
       */}
      <ContextContainer.Provider value={{ appState, updateAppState }}>
        {/* Here we load some child components */}
        <Book title="GoT" price="10" />
        <DebugNotice />
      </ContextContainer.Provider>
    </div>
  );
}

// Child component Book
function Book(props) {
  // Inside the child component you can import whatever the context provider allows.
  // Earlier we passed value={{ appState, updateAppState }}
  // In this child we need the appState and the update handler
  const { appState, updateAppState } = useContext(ContextContainer);

  function handleCommentChange(e) {
    //Here on button click we call updateAppState as we would normally do in the App
    // It adds/updates comment property with input value to the appState
    updateAppState({ ...appState, comment: e.target.value });
  }

  return (
    <div className="book">
      <h2>{props.title}</h2>
      <p>${props.price}</p>
      <input
        type="text"
        //Controlled Component. Value is reverse vound the value of the variable in state
        value={appState.comment}
        onChange={handleCommentChange}
      />
      <br />
      <button
        type="button"
        // Here on button click we call updateAppState as we would normally do in the app
        onClick={() => updateAppState({ ...appState, selected: props.title })}
      >
        Select This Book
      </button>
    </div>
  );
}

// Just another child component
function DebugNotice() {
  // Inside the child component you can import whatever the context provider allows.
  // Earlier we passed value={{ appState, updateAppState }}
  // but in this child we only need the appState to display its value
  const { appState } = useContext(ContextContainer);

  /* Here we pretty print the current state of the appState  */
  return (
    <div className="state">
      <h2>appState</h2>
      <pre>{JSON.stringify(appState, null, 2)}</pre>
    </div>
  );
}

const rootElement = document.body;
ReactDOM.render(<App />, rootElement);

Możesz uruchomić ten przykład w edytorze Code Sandbox.

Edytuj stan przekazywania z kontekstem

J. Źle
źródło
5

Pierwsze rozwiązanie z zachowaniem stanu w komponencie nadrzędnym jest poprawne . Jednak w przypadku bardziej skomplikowanych problemów należy pomyśleć o bibliotece zarządzania stanem , najbardziej popularna jest biblioteka redux używana w funkcji reagowania.

Nesha Zoric
źródło
Zgoda. Moja odpowiedź została odpisana, gdy większość pisała rzeczy w „czystej reakcji”. Przed fluxsplosion.
captray
2

Dziwi mnie, że w chwili pisania nie ma odpowiedzi na proste, idiomatyczne rozwiązanie React. Oto jeden (porównaj rozmiar i złożoność z innymi):

class P extends React.Component {
    state = { foo : "" };

    render(){
        const { foo } = this.state;

        return (
            <div>
                <C1 value={ foo } onChange={ x => this.setState({ foo : x })} />
                <C2 value={ foo } />
            </div>
        )
    }
}

const C1 = ({ value, onChange }) => (
    <input type="text"
           value={ value }
           onChange={ e => onChange( e.target.value ) } />
);

const C2 = ({ value }) => (
    <div>Reacting on value change: { value }</div>
);

Ustawiam stan elementu nadrzędnego na podstawie elementu podrzędnego. Wydaje się, że zdradza to zasadę projektowania React: przepływ danych w jednym kierunku.

Każdy kontrolowanyinput (idiomatyczny sposób pracy z formularzami w React) aktualizuje stan nadrzędny w jegoonChange wywołaniu zwrotnym i nadal niczego nie zdradza.

Przyjrzyj się na przykład składnikowi C1. Czy widzisz jakąś znaczącą różnicę w sposobie?C1 wbudowany inputkomponent obsługuje zmiany stanu? Nie powinieneś, bo nie ma. Podnoszenie stanu i przekazywanie par wartość / onZmień pary jest idiomatyczne dla surowego React. Nie stosowanie referencji, jak sugerują niektóre odpowiedzi.

gaperton
źródło
1
Jakiej wersji reagujesz? Pojawiają się eksperymentalne problemy z właściwością klasy i foo nie jest zdefiniowane.
Isaac Pak
Nie chodziło o wersję reagowania. Kod komponentu był nieprawidłowy. Naprawiono, spróbuj teraz.
gaperton
Oczywiście musisz wyodrębnić element stanu this.state, w renderowaniu brakowało zwrotu, a wiele składników musi być owiniętych w div lub coś takiego. Nie wiem, jak mi to umknęło, kiedy napisałem oryginalną odpowiedź. Musiał być błąd edycji.
gaperton
1
Podoba mi się to rozwiązanie. Jeśli ktoś chce majstrować przy nim, oto piaskownica dla Ciebie.
Isaac Pak
2

Nowsza odpowiedź z przykładem, który wykorzystuje React.useState

Zalecane jest utrzymywanie stanu w komponencie nadrzędnym. Rodzic musi mieć do niego dostęp, ponieważ zarządza nim w dwóch elementach potomnych. Przeniesienie go do stanu globalnego, takiego jak zarządzanego przez Redux, nie jest zalecane z tego samego powodu, dla którego zmienna globalna jest ogólnie gorsza niż lokalna w inżynierii oprogramowania.

Gdy stan znajduje się w komponencie nadrzędnym, dziecko może je mutować, jeśli rodzic podaje dziecko valuei onChangemoduł obsługi w rekwizytach (czasami nazywa się to łączem wartości lub wzorcem łącza stanu ). Oto jak zrobiłbyś to z hakami:


function Parent() {
    var [state, setState] = React.useState('initial input value');
    return <>
        <Child1 value={state} onChange={(v) => setState(v)} />
        <Child2 value={state}>
    </>
}

function Child1(props) {
    return <input
        value={props.value}
        onChange={e => props.onChange(e.target.value)}
    />
}

function Child2(props) {
    return <p>Content of the state {props.value}</p>
}

Cały komponent nadrzędny będzie renderowany ponownie po zmianie danych wejściowych w obiekcie potomnym, co może nie stanowić problemu, jeśli jego ponowne nadanie jest małe / szybkie. Wydajność ponownego renderowania komponentu macierzystego nadal może stanowić problem w ogólnym przypadku (na przykład w dużych formularzach). Ten problem został rozwiązany w twoim przypadku (patrz poniżej).

Wzorzec stanu łącza i brak nadrzędnego renderowania są łatwiejsze do wdrożenia przy użyciu biblioteki innej firmy, takiej jak Hookstate - doładowanieReact.useState obejmuje różne przypadki użycia, w tym również twoją. (Uwaga: Jestem autorem projektu).

Oto jak by to wyglądało z Hookstate. Child1zmieni dane wejściowe, Child2zareaguje na to. Parentbędzie utrzymywać stan, ale nie będzie renderować ponownie po zmianie stanu, tylko Child1i Child2będzie.

import { useStateLink } from '@hookstate/core';

function Parent() {
    var state = useStateLink('initial input value');
    return <>
        <Child1 state={state} />
        <Child2 state={state}>
    </>
}

function Child1(props) {
    // to avoid parent re-render use local state,
    // could use `props.state` instead of `state` below instead
    var state = useStateLink(props.state)
    return <input
        value={state.get()}
        onChange={e => state.set(e.target.value)}
    />
}

function Child2(props) {
    // to avoid parent re-render use local state,
    // could use `props.state` instead of `state` below instead
    var state = useStateLink(props.state)
    return <p>Content of the state {state.get()}</p>
}

PS: istnieje wiele innych przykładów obejmujących podobne i bardziej skomplikowane scenariusze, w tym głęboko zagnieżdżone dane, sprawdzanie poprawności stanu, stan globalny z setStatehakiem itp. Istnieje również pełna przykładowa aplikacja online , która wykorzystuje Hookstate i technikę wyjaśnioną powyżej.

Andrzej
źródło
1

Powinieneś nauczyć się biblioteki Redux i ReactRedux, która uporządkuje twoje stany i rekwizyty w jednym sklepie i będziesz mieć do nich dostęp później w swoich komponentach.

Ramin Taghizada
źródło
1

Za pomocą React> = 16.3 możesz użyć ref i forwardRef, aby uzyskać dostęp do DOM-u dziecka od jego rodzica. Nie używaj już starej metody referencji.
Oto przykład z wykorzystaniem twojego przypadku:

import React, { Component } from 'react';

export default class P extends React.Component {
   constructor (props) {
      super(props)
      this.state = {data: 'test' }
      this.onUpdate = this.onUpdate.bind(this)
      this.ref = React.createRef();
   }

   onUpdate(data) {
      this.setState({data : this.ref.current.value}) 
   }

   render () {
      return (
        <div>
           <C1 ref={this.ref} onUpdate={this.onUpdate}/>
           <C2 data={this.state.data}/>
        </div>
      )
   }
}

const C1 = React.forwardRef((props, ref) => (
    <div>
        <input type='text' ref={ref} onChange={props.onUpdate} />
    </div>
));

class C2 extends React.Component {
    render () {
       return <div>C2 reacts : {this.props.data}</div>
    }
}

Zobacz Refs i ForwardRef, aby uzyskać szczegółowe informacje na temat refs i forwardRef.

Lex Soft
źródło
0
  1. Właściwe jest, aby mieć stan w komponencie nadrzędnym , aby uniknąć odwołania, a co nie
  2. Problemem jest unikanie ciągłej aktualizacji wszystkich dzieci podczas pisania w polu
  3. Dlatego każde dziecko powinno być komponentem (jak nie w PureComponent) i implementować shouldComponentUpdate(nextProps, nextState)
  4. W ten sposób podczas pisania w polu formularza aktualizuje się tylko to pole

Poniższy kod używa @boundadnotacji z ES.Next babel-plugin-transform-decorators-legacy z BabelJS 6 i właściwości klasy (adnotacja ustawia tę wartość na funkcjach składowych podobnych do wiązania):

/*
© 2017-present Harald Rudell <[email protected]> (http://www.haraldrudell.com)
All rights reserved.
*/
import React, {Component} from 'react'
import {bound} from 'class-bind'

const m = 'Form'

export default class Parent extends Component {
  state = {one: 'One', two: 'Two'}

  @bound submit(e) {
    e.preventDefault()
    const values = {...this.state}
    console.log(`${m}.submit:`, values)
  }

  @bound fieldUpdate({name, value}) {
    this.setState({[name]: value})
  }

  render() {
    console.log(`${m}.render`)
    const {state, fieldUpdate, submit} = this
    const p = {fieldUpdate}
    return (
      <form onSubmit={submit}> {/* loop removed for clarity */}
        <Child name='one' value={state.one} {...p} />
        <Child name='two' value={state.two} {...p} />
        <input type="submit" />
      </form>
    )
  }
}

class Child extends Component {
  value = this.props.value

  @bound update(e) {
    const {value} = e.target
    const {name, fieldUpdate} = this.props
    fieldUpdate({name, value})
  }

  shouldComponentUpdate(nextProps) {
    const {value} = nextProps
    const doRender = value !== this.value
    if (doRender) this.value = value
    return doRender
  }

  render() {
    console.log(`Child${this.props.name}.render`)
    const {value} = this.props
    const p = {value}
    return <input {...p} onChange={this.update} />
  }
}
Harald Rudell
źródło
0

Pojęcie przekazywania danych z rodzica na dziecko i odwrotnie jest wyjaśnione.

import React, { Component } from "react";
import ReactDOM from "react-dom";

// taken refrence from https://gist.github.com/sebkouba/a5ac75153ef8d8827b98

//example to show how to send value between parent and child

//  props is the data which is passed to the child component from the parent component

class Parent extends Component {
  constructor(props) {
    super(props);

    this.state = {
      fieldVal: ""
    };
  }

  onUpdateParent = val => {
    this.setState({
      fieldVal: val
    });
  };

  render() {
    return (
      // To achieve the child-parent communication, we can send a function
      // as a Prop to the child component. This function should do whatever
      // it needs to in the component e.g change the state of some property.
      //we are passing the function onUpdateParent to the child
      <div>
        <h2>Parent</h2>
        Value in Parent Component State: {this.state.fieldVal}
        <br />
        <Child onUpdate={this.onUpdateParent} />
        <br />
        <OtherChild passedVal={this.state.fieldVal} />
      </div>
    );
  }
}

class Child extends Component {
  constructor(props) {
    super(props);

    this.state = {
      fieldValChild: ""
    };
  }

  updateValues = e => {
    console.log(e.target.value);
    this.props.onUpdate(e.target.value);
    // onUpdateParent would be passed here and would result
    // into onUpdateParent(e.target.value) as it will replace this.props.onUpdate
    //with itself.
    this.setState({ fieldValChild: e.target.value });
  };

  render() {
    return (
      <div>
        <h4>Child</h4>
        <input
          type="text"
          placeholder="type here"
          onChange={this.updateValues}
          value={this.state.fieldVal}
        />
      </div>
    );
  }
}

class OtherChild extends Component {
  render() {
    return (
      <div>
        <h4>OtherChild</h4>
        Value in OtherChild Props: {this.props.passedVal}
        <h5>
          the child can directly get the passed value from parent by this.props{" "}
        </h5>
      </div>
    );
  }
}

ReactDOM.render(<Parent />, document.getElementById("root"));

akshay
źródło
1
Proponuję poinformować właściciela poprzedniego odpowiedzi jako komentarz, aby dodać opis podany przez niego w jego odpowiedzi. Następnie przekazanie imienia i nazwiska za uprzejmość.
Thomas Easo,
@ThomasEaso Nie mogę go tutaj znaleźć, sprawdziłem to. Czy są jakieś inne sugestie
akshay
kliknij przycisk „edytuj” po lewej stronie jego ikony i zmodyfikuj odpowiedź i prześlij. przejdzie do moderatora i zrobi wszystko, co konieczne, aby uzyskać cenne komentarze.
Thomas Easo,