Qual é a maneira correta de passar o estado do elemento de formulário para elementos irmãos / pais?

186
  • Suponha que eu possua uma classe React P, que renderize duas classes filho, C1 e C2.
  • C1 contém um campo de entrada. Vou me referir a este campo de entrada como Foo.
  • Meu objetivo é deixar o C2 reagir às mudanças no Foo.

Eu vim com duas soluções, mas nenhuma delas parece estar certa.

Primeira solução:

  1. Atribua P um estado state.input,.
  2. Crie uma onChangefunção em P, que recebe um evento e define state.input.
  3. Passe isso onChangepara C1 como a propse deixe C1 ligar this.props.onChange- se ao onChangede Foo.

Isso funciona. Sempre que o valor de Foo muda, ele dispara a setStateem P, então P terá a entrada para passar para C2.

Mas não parece certo pelo mesmo motivo: estou definindo o estado de um elemento pai a partir de um elemento filho. Isso parece trair o princípio de design do React: fluxo de dados de direção única.
É assim que eu devo fazê-lo ou há uma solução mais natural para reagir?

Segunda solução:

Basta colocar Foo em P.

Mas esse é um princípio de design que devo seguir quando estruturar meu aplicativo - colocando todos os elementos de formulário na renderclasse de nível mais alto?

Como no meu exemplo, se eu tiver uma renderização grande de C1, não quero colocar todo o renderC1 em renderP apenas porque C1 tem um elemento de formulário.

Como devo fazer isso?

octref
fonte
Estou prestes a fazer exatamente a mesma coisa e, apesar de ele está funcionando corretamente, tenho a sensação de que é apenas um truque gigante
Mirko

Respostas:

198

Então, se estou entendendo corretamente, sua primeira solução está sugerindo que você esteja mantendo o estado em seu componente raiz? Não posso falar pelos criadores do React, mas geralmente acho que essa é uma solução adequada.

Manter o estado é uma das razões (pelo menos eu acho) que o React foi criado. Se você já implementou seu próprio lado do cliente de padrão de estado para lidar com uma interface de usuário dinâmica que possui muitas peças móveis interdependentes, você vai adorar o React, porque alivia muito dessa dor de gerenciamento de estado.

Mantendo o estado ainda mais na hierarquia e atualizando-o através de eventos, seu fluxo de dados ainda é praticamente unidirecional; você está apenas respondendo a eventos no componente Raiz; na verdade, não está obtendo os dados por meio de ligação bidirecional, você está dizendo ao componente Raiz que "ei, algo aconteceu aqui, verifique os valores" ou está passando o estado de alguns dados no componente filho para atualizar o estado. Você alterou o estado em C1 e deseja que o C2 esteja ciente disso, portanto, atualizando o estado no componente Raiz e renderizando novamente, os objetos de suporte do C2 agora estão sincronizados desde que o estado foi atualizado no componente Raiz e transmitido adiante .

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
fonte
5
Sem problemas. Na verdade, voltei depois de escrever este post e reler parte da documentação, e parece estar alinhado com o pensamento e as práticas recomendadas. O React possui uma documentação realmente excelente e, toda vez que eu acabo me perguntando para onde algo deve ir, eles geralmente o abordam em algum lugar nos documentos. Confira a seção sobre estado aqui, facebook.github.io/react/docs/…
captray
@captray, mas e se C2tiver um getInitialStatepara dados e dentro do que renderele usa this.state.data?
Dmitry Polushkin
2
@DmitryPolushkin Se estou entendendo sua pergunta corretamente, você deseja passar dados do seu Componente Raiz para o C2 como acessórios. No C2, esses dados seriam definidos como estado inicial (por exemplo, getInitialState: function () {return {someData: this.props.dataFromParentThatWillChange}} e você desejará implementar o componentWillReceiveProps e chamar this.setState com os novos adereços para atualizar o . estado em C2 Desde que eu inicialmente respondeu: Estou usando o Flux, e recomendo que você olhar para ele, bem faz sua componentes mais limpo e vai mudar a maneira de pensar sobre o estado..
captray
3
@DmitryPolushkin Eu queria postar isso para acompanhamento. facebook.github.io/react/tips/… Tudo bem desde que você saiba o que está fazendo e saiba que os dados serão alterados, mas em muitas situações você provavelmente poderá mudar as coisas. Também é importante observar que você não precisa criar isso como uma hierarquia. Você pode montar C1 e C2 em locais diferentes no DOM e os dois podem ouvir eventos de alteração em alguns dados. Vejo muitas pessoas pressionando por componentes hierárquicos quando não precisam deles.
captray
5
O código acima tem 2 bugs - ambos envolvendo não vincular "this" no contexto correto, eu fiz as correções acima e também para quem precisa de uma demonstração do codepen
Alex H
34

Depois de usar o React para criar um aplicativo agora, gostaria de compartilhar algumas reflexões sobre esta pergunta que fiz há meio ano.

Eu recomendo que você leia

A primeira postagem é extremamente útil para entender como você deve estruturar seu aplicativo React.

O Flux responde à pergunta por que você deve estruturar seu aplicativo React dessa maneira (em vez de como estruturá-lo). A reação é de apenas 50% do sistema, e com o Flux você pode ver toda a imagem e ver como eles constituem um sistema coerente.

Voltar para a pergunta.

Quanto à minha primeira solução, não há problema em deixar o manipulador ir na direção inversa, pois os dados ainda estão indo na direção única.

No entanto, se deixar um manipulador acionar um setState em P pode estar certo ou errado, dependendo da sua situação.

Se o aplicativo for um simples conversor Markdown, sendo C1 a entrada bruta e C2 a saída HTML, não há problema em deixar C1 acionar um setState em P, mas alguns podem argumentar que essa não é a maneira recomendada de fazer isso.

No entanto, se o aplicativo for uma lista de tarefas, C1 sendo a entrada para a criação de uma nova tarefa, C2 a lista de tarefas em HTML, você provavelmente desejará que o manipulador suba dois níveis acima de P - para o dispatcherque permite a storeatualização data store, que envia os dados para P e preenche as visualizações. Veja o artigo do Flux. Aqui está um exemplo: Flux - TodoMVC

Geralmente, prefiro a maneira descrita no exemplo da lista de tarefas. Quanto menos estado você tiver no seu aplicativo, melhor.

octref
fonte
7
Eu frequentemente discuto isso em apresentações sobre React e Flux. O ponto que eu tento enfatizar é o que você descreveu acima, e é a separação entre o estado de exibição e o estado do aplicativo. Há situações em que as coisas podem passar de apenas estado de exibição para estado de aplicativo, especialmente em situações em que você está salvando o estado da interface do usuário (como valores predefinidos). Eu acho incrível que você tenha voltado com seus próprios pensamentos um ano depois. +1
captray 11/08/2015
@captray Então, podemos dizer que o redux é poderoso do que reagir em todas as circunstâncias? Apesar de sua curva de aprendizagem ... (vindo de Java)
Bruce Sun
Não sei bem o que você quer dizer. Eles lidam muito bem com duas coisas diferentes e são uma separação adequada de preocupações.
captray
1
Temos um projeto usando redux e, meses depois, parece que ações são usadas para tudo. É como essa grande mistura de código espaguete e impossível de entender, um desastre total. A administração do estado pode ser boa, mas possivelmente seriamente incompreendida. É seguro assumir que fluxo / redux deve ser usado apenas para partes do estado que precisam ser acessadas globalmente?
wayofthefuture
1
@wayofthefuture Sem ver seu código, posso dizer que vejo muitos espaguetes Reagir como um bom jQuery de espaguete velho. O melhor conselho que posso oferecer é tentar seguir o SRP. Torne seus componentes o mais simples possível; componentes de renderização burros, se puder. Também envio por abstrações como um componente <DataProvider />. Faz uma coisa bem. Forneça dados. Isso geralmente se torna o componente "raiz" e transmite dados, aproveitando os filhos (e a clonagem) como acessórios (contrato definido). Por fim, tente pensar primeiro no servidor. Isso tornará seu JS muito mais limpo, com melhores estruturas de dados.
captray
6

Cinco anos depois, com a introdução dos React Hooks, agora existe uma maneira muito mais elegante de fazê-lo com o uso do useContext hook.

Você define o contexto em um escopo global, exporta variáveis, objetos e funções no componente pai e, em seguida, agrupa os filhos no aplicativo em um contexto fornecido e importa o que for necessário nos componentes filhos. Abaixo está uma prova de conceito.

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

Você pode executar este exemplo no editor de Code Sandbox.

Editar estado de passagem com contexto

J. Errado
fonte
5

A primeira solução, mantendo o estado no componente pai , é a correta . No entanto, para problemas mais complexos, você deve pensar em alguma biblioteca de gerenciamento de estado , redux é o mais popular usado com o react.

Nesha Zoric
fonte
Acordado. Minha resposta foi escrita quando a maioria estava escrevendo coisas em "pure React". Antes da explosão do fluxo.
captray
2

Estou surpreso que não haja respostas com uma solução React idiomática direta no momento em que estou escrevendo. Então, aqui está (compare o tamanho e a complexidade com os outros):

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

Estou definindo o estado de um elemento pai a partir de um elemento filho. Isso parece trair o princípio de design do React: fluxo de dados de direção única.

Qualquer controleinput (maneira idiomática de trabalhar com formulários no React) atualiza o estado pai em seu onChangeretorno de chamada e ainda não trai nada.

Observe atentamente o componente C1, por exemplo. Você vê alguma diferença significativa na forma como C1e built-in inputalça componente as mudanças de estado? Você não deveria, porque não há. A elevação do estado e a transmissão de pares value / onChange são idiomáticas para o React bruto. Não uso de referências, como sugerem algumas respostas.

gaperton
fonte
1
Qual versão do react você está usando? Estou recebendo problemas de propriedade de classe experimental e foo não está definido.
Isaac Pak
Não se tratava de uma versão do reagir. O código do componente estava errado. Corrigido, tente agora.
39519 Gaperton
Obviamente, você precisa extrair membro do estado this.state, o retorno estava ausente na renderização e vários componentes devem ser agrupados em div ou algo assim. Não sei como eu perdi isso quando escrevi a resposta original. Deve ter sido o erro de edição.
gaperton
1
Eu gosto desta solução. Se alguém quiser mexer com ele, aqui está uma caixa de areia para você.
Isaac Pak
2

Resposta mais recente com um exemplo, que usa React.useState

Manter o estado no componente pai é a maneira recomendada. O pai precisa ter acesso a ele, pois o gerencia em dois componentes filhos. Mover para o estado global, como o gerenciado pelo Redux, não é recomendado pelo mesmo motivo pelo qual a variável global é pior do que a local em geral na engenharia de software.

Quando o estado está no componente pai, o filho pode modificá-lo se o pai fornecer o filho valuee o onChangemanipulador em adereços (às vezes é chamado de link de valor ou padrão de link de estado ). Aqui está como você faria isso com ganchos:


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

Todo o componente pai será renderizado novamente na alteração de entrada no filho, o que pode não ser um problema se o componente pai for pequeno / rápido para ser renderizado novamente. O desempenho de re-renderização do componente pai ainda pode ser um problema no caso geral (por exemplo, formulários grandes). Este é o problema resolvido no seu caso (veja abaixo).

O padrão de link de estado e nenhuma re-renderização pai são mais fáceis de implementar usando a biblioteca de terceiros, como Hookstate - sobrecarregada React.useStatepara cobrir diversos casos de uso, incluindo o seu. (Aviso: sou autor do projeto).

Aqui está como ficaria com o Hookstate. Child1mudará a entrada, Child2reagirá a ela. Parentmanterá o estado, mas não será renderizado novamente, somente Child1e Child2continuará.

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: existem muitos outros exemplos aqui, cobrindo cenários semelhantes e mais complicados, incluindo dados profundamente aninhados, validação de estado, estado global com setStategancho, etc. Há também um aplicativo de amostra completo online , que usa o Hookstate e a técnica explicada acima.

Andrew
fonte
1

Você deve aprender as bibliotecas Redux e ReactRedux. Ele estruturará seus estados e objetos em uma loja e você poderá acessá-los posteriormente em seus componentes.

Ramin Taghizada
fonte
1

Com React> = 16.3, você pode usar ref e forwardRef, para obter acesso ao DOM da criança a partir do pai. Não use mais o modo antigo de referências.
Aqui está o exemplo usando o seu caso:

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

Consulte Refs e ForwardRef para obter informações detalhadas sobre refs e forwardRef.

Lex Soft
fonte
0
  1. A coisa certa a fazer é ter o estado no componente pai , para evitar ref e o que não
  2. Um problema é evitar atualizar constantemente todos os filhos ao digitar em um campo
  3. Portanto, cada filho deve ser um componente (como não é um PureComponent) e implementar shouldComponentUpdate(nextProps, nextState)
  4. Dessa forma, ao digitar em um campo de formulário, apenas esse campo é atualizado

O código abaixo usa @boundanotações do ES.Next babel-plugin-transform-decorators-legacy do BabelJS 6 e propriedades de classe (a anotação define esse valor nas funções de membros semelhantes à ligação):

/*
© 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
fonte
0

O conceito de passar dados de pai para filho e vice-versa é explicado.

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
fonte
1
Eu sugiro que você informe o proprietário da resposta anterior como um comentário para adicionar a descrição fornecida por você na resposta dele. Em seguida, refira seu nome para uma cortesia.
Thomas Easo
@ThomasEaso Não consigo encontrá-lo aqui, verifiquei. Existe alguma outra sugestão
Akshay 28/08
clique no botão "editar" à esquerda do ícone e modifique a resposta e envie. ele será direcionado a um moderador e eles farão o necessário para seus valiosos comentários.
Thomas Easo