Apa cara yang benar untuk menyampaikan status elemen formulir ke elemen saudara / orang tua?


186
  • Misalkan saya memiliki kelas B React, yang membuat dua kelas anak, C1 dan C2.
  • C1 berisi bidang input. Saya akan merujuk ke bidang input ini sebagai Foo.
  • Tujuan saya adalah membiarkan C2 bereaksi terhadap perubahan di Foo.

Saya telah menemukan dua solusi, tetapi tidak satu pun dari keduanya terasa benar.

Solusi pertama:

  1. Tetapkan P status state.input,.
  2. Buat onChangefungsi dalam P, yang mengambil acara dan set state.input.
  3. Berikan ini onChangeke C1 sebagai props, dan biarkan C1 mengikat this.props.onChangeke onChangeFoo.

Ini bekerja. Setiap kali nilai Foo berubah, ia memicu a setStatedi P, jadi P akan memiliki input untuk diteruskan ke C2.

Tapi rasanya tidak tepat karena alasan yang sama: Saya mengatur keadaan elemen induk dari elemen anak. Ini tampaknya mengkhianati prinsip desain React: aliran data satu arah.
Apakah ini yang seharusnya saya lakukan, atau adakah solusi yang lebih alami?

Solusi kedua:

Masukkan saja Foo ke P.

Tetapi apakah ini prinsip desain yang harus saya ikuti ketika saya menyusun aplikasi saya — menempatkan semua elemen bentuk di renderkelas tingkat tertinggi?

Seperti dalam contoh saya, jika saya memiliki rendering besar C1, saya benar-benar tidak ingin menempatkan seluruh renderC1 ke renderP hanya karena C1 memiliki elemen bentuk.

Bagaimana saya harus melakukannya?


Saya akan melakukan hal yang persis sama dan, meskipun bekerja dengan baik, saya merasa itu hanya hack raksasa
Mirko

Jawaban:


198

Jadi, jika saya memahami Anda dengan benar, solusi pertama Anda adalah menyarankan Anda menjaga status pada komponen root Anda? Saya tidak dapat berbicara untuk pencipta React, tetapi secara umum, saya menemukan ini sebagai solusi yang tepat.

Mempertahankan keadaan adalah salah satu alasan (setidaknya saya pikir) bahwa Bereaksi diciptakan. Jika Anda pernah menerapkan sisi klien pola negara Anda sendiri untuk berurusan dengan UI dinamis yang memiliki banyak bagian bergerak yang saling bergantung, maka Anda akan menyukai Bereaksi, karena hal ini mengurangi banyak rasa sakit manajemen negara bagian ini.

Dengan menjaga status lebih lanjut dalam hierarki, dan memperbaruinya melalui acara, aliran data Anda masih cukup banyak searah, Anda hanya merespons peristiwa di komponen Root, Anda tidak benar-benar mendapatkan data di sana melalui ikatan dua arah, Anda memberi tahu komponen Root bahwa "hei, sesuatu terjadi di sini, periksa nilainya" atau Anda melewatkan status beberapa data dalam komponen anak untuk memperbarui status. Anda mengubah status di C1, dan Anda ingin C2 menyadarinya, jadi, dengan memperbarui status di komponen Root dan rendering ulang, properti C2 sekarang disinkronkan karena keadaan diperbarui di komponen Root dan diteruskan .

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)

5
Tidak masalah. Saya benar-benar kembali setelah menulis posting ini dan membaca kembali beberapa dokumentasi, dan tampaknya sejalan dengan pemikiran dan praktik terbaik mereka. Bereaksi memiliki dokumentasi yang sangat bagus, dan setiap kali saya bertanya-tanya ke mana harus pergi, mereka biasanya membahasnya di suatu tempat di dokumen. Lihat bagian tentang keadaan di sini, facebook.github.io/react/docs/…
captray

@captray tetapi bagaimana, jika C2memiliki getInitialStatedata dan di renderdalamnya digunakan this.state.data?
Dmitry Polushkin

2
@DmitryPolushkin Jika saya memahami pertanyaan Anda dengan benar, Anda ingin meneruskan data dari Komponen Root Anda ke C2 sebagai alat peraga. Di C2, data itu akan ditetapkan sebagai keadaan awal (yaitu getInitialState: function () {return {someData: this.props.dataFromParentThatWillChange}} dan Anda ingin mengimplementasikan componentWillReceiveProps dan memanggil this.setState dengan props baru untuk memperbarui negara dalam C2. Sejak saya awalnya menjawab, saya telah menggunakan Flux, dan akan sangat menyarankan Anda melihatnya juga. Itu membuat komponen Anda lebih bersih dan akan mengubah cara Anda berpikir tentang negara.
captray

3
@DmitryPolushkin Saya ingin memposting ini untuk tindak lanjut. facebook.github.io/react/tips/… Tidak masalah selama Anda tahu apa yang Anda lakukan dan Anda tahu bahwa data akan berubah, tetapi dalam banyak situasi Anda mungkin dapat memindahkan banyak hal. Penting juga untuk dicatat bahwa Anda tidak harus membangun ini sebagai hierarki. Anda dapat memasang C1 dan C2 di berbagai tempat di DOM, dan keduanya dapat mendengarkan perubahan acara pada beberapa data. Saya melihat banyak orang mendorong komponen hierarkis ketika mereka tidak membutuhkannya.
captray

5
Kode di atas memiliki 2 bug - keduanya melibatkan tidak mengikat "ini" dalam konteks yang benar, saya telah membuat koreksi di atas dan juga bagi siapa saja yang membutuhkan demonstrasi codepen
Alex H

34

Setelah menggunakan React untuk membangun aplikasi sekarang, saya ingin berbagi pemikiran tentang pertanyaan ini yang saya tanyakan setengah tahun yang lalu.

Saya sarankan Anda untuk membaca

Posting pertama sangat membantu untuk memahami bagaimana Anda harus menyusun aplikasi Bereaksi Anda.

Flux menjawab pertanyaan mengapa Anda harus menyusun aplikasi Bereaksi Anda dengan cara ini (berbeda dengan cara menyusunnya). Bereaksi hanya 50% dari sistem, dan dengan Flux Anda bisa melihat seluruh gambar dan melihat bagaimana mereka membentuk sistem yang koheren.

Kembali ke pertanyaan.

Adapun solusi pertama saya, benar-benar OK untuk membiarkan pawang berbalik arah, karena data masih berjalan satu arah.

Namun, apakah membiarkan pawang memicu setState di P dapat benar atau salah tergantung pada situasi Anda.

Jika aplikasi ini adalah konverter Markdown sederhana, C1 menjadi input mentah dan C2 menjadi output HTML, tidak apa-apa membiarkan C1 memicu setState di P, tetapi beberapa orang mungkin berpendapat ini bukan cara yang disarankan untuk melakukannya.

Namun, jika aplikasi adalah daftar todo, C1 menjadi input untuk membuat todo baru, C2 daftar todo dalam HTML, Anda mungkin ingin menangani untuk naik dua tingkat daripada P - ke dispatcher, yang memungkinkan storepembaruan data store, yang kemudian mengirim data ke P dan mengisi tampilan. Lihat artikel Flux itu. Berikut ini sebuah contoh: Flux - TodoMVC

Secara umum, saya lebih suka cara yang dijelaskan dalam contoh daftar todo. Semakin sedikit status yang Anda miliki di aplikasi Anda, semakin baik.


7
Saya sering membahas hal ini dalam presentasi tentang React dan Flux. Poin yang saya coba tekankan adalah apa yang telah Anda jelaskan di atas, dan itu adalah pemisahan Status Tampilan dan Status Aplikasi. Ada situasi di mana hal-hal dapat transisi dari hanya menjadi View State menjadi Application Application, terutama dalam situasi di mana Anda menyimpan keadaan UI (seperti nilai preset). Saya pikir itu luar biasa bahwa Anda kembali dengan pikiran Anda sendiri setahun kemudian. +1
captray

@captray Jadi dapatkah kita mengatakan bahwa redux lebih kuat daripada bereaksi dalam semua keadaan? Meskipun kurva belajar mereka ... (berasal dari Jawa)
Bruce Sun

Saya tidak yakin apa yang Anda maksud. Mereka menangani dua hal berbeda dengan sangat baik, dan merupakan pemisahan keprihatinan yang tepat.
Captray

1
Kami memiliki proyek menggunakan redux, dan berbulan-bulan ke dalamnya, sepertinya tindakan digunakan untuk semuanya. Ini seperti campuran besar kode spageti dan mustahil dipahami, benar-benar musibah. Manajemen negara mungkin baik, tetapi mungkin sangat disalahpahami. Apakah aman untuk mengasumsikan bahwa fluks / reduks hanya boleh digunakan untuk bagian negara yang perlu diakses secara global?
wayofthefuture

1
@ wayofthefuture Tanpa melihat kode Anda, saya dapat mengatakan bahwa saya melihat banyak spageti Bereaksi seperti jQuery ole spaghetti yang baik. Saran terbaik yang bisa saya tawarkan adalah mencoba dan mengikuti SRP. Buat komponen Anda sesederhana mungkin; komponen rendering bodoh jika Anda bisa. Saya juga mendorong abstraksi seperti komponen <DataProvider />. Itu melakukan satu hal dengan baik. Berikan data. Ini biasanya menjadi komponen 'root' dan meneruskan data dengan memanfaatkan anak-anak (dan kloning) sebagai alat peraga (kontrak yang ditentukan). Pada akhirnya, cobalah untuk berpikir tentang Server terlebih dahulu. Ini akan membuat JS Anda jauh lebih bersih dengan struktur data yang lebih baik.
captray

6

Lima tahun kemudian dengan diperkenalkannya React Hooks sekarang ada cara yang jauh lebih elegan untuk melakukannya dengan menggunakan useContext hook.

Anda mendefinisikan konteks dalam lingkup global, mengekspor variabel, objek dan fungsi dalam komponen induk dan kemudian membungkus anak-anak di App dalam konteks yang disediakan dan mengimpor apa pun yang Anda butuhkan dalam komponen anak. Di bawah ini adalah bukti konsep.

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

Anda dapat menjalankan contoh ini di editor Code Sandbox.

Edit status lewat dengan konteks


5

Solusi pertama, dengan menjaga status dalam komponen induk , adalah yang benar . Namun, untuk masalah yang lebih kompleks, Anda harus berpikir tentang beberapa perpustakaan manajemen negara , redux adalah yang paling populer digunakan dengan reaksi.


Sepakat. Tanggapan saya ditulis kembali ketika kebanyakan menulis hal-hal dalam "murni Bereaksi". Sebelum terjadinya fluks.
captray

2

Saya terkejut bahwa tidak ada jawaban dengan solusi Bereaksi idiomatis langsung pada saat saya sedang menulis. Jadi ini dia (bandingkan ukuran dan kompleksitasnya dengan yang lain):

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

Saya mengatur keadaan elemen induk dari elemen anak. Ini tampaknya mengkhianati prinsip desain React: aliran data satu arah.

Setiap cara terkendaliinput (cara kerja idiomatis dengan formulir di Bereaksi) memperbarui status induk dalam onChangepanggilan baliknya dan masih tidak mengkhianati apa pun.

Perhatikan baik-baik komponen C1, misalnya. Apakah Anda melihat perbedaan yang signifikan dalam cara bagaimana C1dan built-in inputmenangani komponen perubahan negara? Anda tidak boleh, karena tidak ada. Mengangkat status dan meneruskan nilai / onChange pair adalah idiomatik untuk Bereaksi mentah. Tidak menggunakan referensi, seperti yang disarankan beberapa jawaban.


1
Versi reaksi apa yang Anda gunakan? Saya mendapatkan masalah properti kelas eksperimental dan foo tidak didefinisikan.
Isaac Pak

Itu bukan tentang versi reaksi. Kode komponen salah. Perbaiki, coba sekarang.
gaperton

Jelas, Anda harus mengekstrak anggota negara dari this.state, kembalinya hilang dalam render, dan beberapa komponen harus dibungkus dengan div atau sesuatu. Tidak tahu bagaimana saya melewatkan itu ketika saya menulis jawaban yang asli. Pasti karena kesalahan pengeditan.
gaperton

1
Saya suka solusi ini. Jika ada yang ingin mengotak-atiknya, inilah kotak pasir untuk Anda.
Isaac Pak

2

Jawaban yang lebih baru dengan contoh, yang menggunakan React.useState

Menjaga status dalam komponen induk adalah cara yang disarankan. Orang tua perlu memiliki akses ke sana karena mengaturnya di dua komponen anak-anak. Memindahkannya ke kondisi global, seperti yang dikelola oleh Redux, tidak direkomendasikan untuk alasan yang sama mengapa variabel global lebih buruk daripada lokal pada umumnya dalam rekayasa perangkat lunak.

Ketika status berada dalam komponen induk, anak dapat memutasinya jika orangtua memberikan anak valuedan onChangepawang dalam alat peraga (kadang-kadang disebut tautan nilai atau pola tautan negara ). Inilah cara Anda melakukannya dengan kait:


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

Seluruh komponen induk akan merender ulang pada input input pada anak, yang mungkin tidak menjadi masalah jika komponen induk kecil / cepat untuk dirender ulang. Kinerja ulang membuat komponen induk masih bisa menjadi masalah dalam kasus umum (misalnya bentuk besar). Ini masalah yang diselesaikan dalam kasus Anda (lihat di bawah).

Pola tautan negara dan tidak ada render ulang orang tua lebih mudah diterapkan menggunakan perpustakaan pihak ke-3, seperti Hookstate - supercharged React.useStateuntuk mencakup berbagai kasus penggunaan, termasuk yang Anda miliki . (Penafian: Saya seorang penulis proyek).

Begini tampilannya dengan Hookstate. Child1akan mengubah input, Child2akan bereaksi padanya. Parentakan memegang negara tetapi tidak akan membuat kembali saat perubahan negara, hanya Child1dan Child2akan.

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: ada banyak contoh di sini yang mencakup skenario yang serupa dan lebih rumit, termasuk data yang bersarang dalam, validasi negara, keadaan global dengan setStatekait, dll. Ada juga contoh aplikasi online lengkap , yang menggunakan Hookstate dan teknik yang dijelaskan di atas.


1

Anda harus mempelajari pustaka Redux dan ReactRedux. Ini akan menyusun status dan properti Anda di satu toko dan Anda dapat mengaksesnya nanti di komponen Anda.


1

Dengan React> = 16.3 Anda dapat menggunakan ref dan forwardRef, untuk mendapatkan akses ke DOM anak dari induknya. Jangan menggunakan referensi cara lama lagi.
Berikut ini contoh penggunaan kasing Anda:

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

Lihat Ref dan ForwardRef untuk info detail tentang ref dan forwardRef.


0
  1. Hal yang benar untuk dilakukan adalah memiliki status dalam komponen induk , untuk menghindari ref dan apa yang tidak
  2. Masalahnya adalah untuk terus-menerus memperbarui semua anak saat mengetik ke bidang
  3. Oleh karena itu, setiap anak harus menjadi Komponen (seperti bukan PureComponent) dan mengimplementasikan shouldComponentUpdate(nextProps, nextState)
  4. Dengan cara ini, saat mengetik ke bidang formulir, hanya bidang itu yang diperbarui

Kode di bawah ini menggunakan @boundanotasi dari ES. Berikutnya babel-plugin-transform-decorators-legacy dari BabelJS 6 dan properti-kelas (anotasi menetapkan nilai ini pada fungsi anggota yang mirip dengan bind):

/*
© 2017-present Harald Rudell <harald.rudell@gmail.com> (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} />
  }
}

0

Konsep menyampaikan data dari orang tua ke anak dan sebaliknya dijelaskan.

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


1
Saya menyarankan Anda untuk memberi tahu pemilik jawaban sebelumnya sebagai komentar untuk menambahkan deskripsi yang Anda berikan dalam jawabannya. Diikuti dengan mereferensikan nama Anda untuk rasa hormat.
Thomas Easo

@ThomasEaso Saya tidak bisa menemukannya di sini, saya memeriksanya. Apakah ada saran lain
akshay

klik tombol "edit" di sebelah kiri ikonnya dan modifikasi jawabannya dan kirim. itu akan pergi ke moderator dan mereka akan melakukan yang diperlukan untuk komentar Anda yang berharga.
Thomas Easo
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.