React input defaultValue no se actualiza con el estado


94

Estoy tratando de crear un formulario simple con reaccionar, pero tengo dificultades para que los datos se vinculen correctamente con el valor predeterminado del formulario.

El comportamiento que estoy buscando es este:

  1. Cuando abro mi página, el campo de entrada de texto debe llenarse con el texto de mi AwayMessage en mi base de datos. Eso es "Texto de muestra"
  2. Idealmente, quiero tener un marcador de posición en el campo de entrada de texto si el AwayMessage en mi base de datos no tiene texto.

Sin embargo, en este momento, veo que el campo de entrada de texto está en blanco cada vez que actualizo la página. (Aunque lo que escribo en la entrada se guarda correctamente y persiste). Creo que esto se debe a que el html del campo de texto de entrada se carga cuando AwayMessage es un objeto vacío, pero no se actualiza cuando se carga awayMessage. Además, no puedo especificar un valor predeterminado para el campo.

Eliminé parte del código para mayor claridad (es decir, onToggleChange)

    window.Pages ||= {}

    Pages.AwayMessages = React.createClass

      getInitialState: ->
        App.API.fetchAwayMessage (data) =>
        @setState awayMessage:data.away_message
        {awayMessage: {}}

      onTextChange: (event) ->
        console.log "VALUE", event.target.value

      onSubmit: (e) ->
        window.a = @
        e.preventDefault()
        awayMessage = {}
        awayMessage["master_toggle"]=@refs["master_toggle"].getDOMNode().checked
        console.log "value of text", @refs["text"].getDOMNode().value
        awayMessage["text"]=@refs["text"].getDOMNode().value
        @awayMessage(awayMessage)

      awayMessage: (awayMessage)->
        console.log "I'm saving", awayMessage
        App.API.saveAwayMessage awayMessage, (data) =>
          if data.status == 'ok'
            App.modal.closeModal()
            notificationActions.notify("Away Message saved.")
            @setState awayMessage:awayMessage

      render: ->
        console.log "AWAY_MESSAGE", this.state.awayMessage
        awayMessageText = if this.state.awayMessage then this.state.awayMessage.text else "Placeholder Text"
        `<div className="away-messages">
           <div className="header">
             <h4>Away Messages</h4>
           </div>
           <div className="content">
             <div className="input-group">
               <label for="master_toggle">On?</label>
               <input ref="master_toggle" type="checkbox" onChange={this.onToggleChange} defaultChecked={this.state.awayMessage.master_toggle} />
             </div>
             <div className="input-group">
               <label for="text">Text</label>
               <input ref="text" onChange={this.onTextChange} defaultValue={awayMessageText} />
             </div>
           </div>
           <div className="footer">
             <button className="button2" onClick={this.close}>Close</button>
             <button className="button1" onClick={this.onSubmit}>Save</button>
           </div>
         </div>

my console.log para AwayMessage muestra lo siguiente:

AWAY_MESSAGE Object {}
AWAY_MESSAGE Object {id: 1, company_id: 1, text: "Sample Text", master_toggle: false}

Respuestas:


61

defaultValue es solo para la carga inicial

Si desea inicializar la entrada, debe usar defaultValue, pero si desea usar el estado para cambiar el valor, debe usar value. Personalmente, me gusta usar defaultValue si solo lo estoy inicializando y luego solo uso refs para obtener el valor cuando lo envío. Hay más información sobre referencias y entradas en los documentos de reacción, https://facebook.github.io/react/docs/forms.html y https://facebook.github.io/react/docs/working-with-the- browser.html .

Así es como reescribiría su entrada:

awayMessageText = if this.state.awayMessage then this.state.awayMessage.text else ''
<input ref="text" onChange={this.onTextChange} placeholder="Placeholder Text" value={@state.awayMessageText} />

Además, no desea pasar texto de marcador de posición como lo hizo porque eso realmente establecerá el valor en 'texto de marcador de posición'. Aún necesita pasar un valor en blanco a la entrada porque undefined y nil convierte el valor en defaultValue esencialmente. https://facebook.github.io/react/tips/controlled-input-null-value.html .

getInitialState no puede realizar llamadas a la API

Debe realizar llamadas a la API después de ejecutar getInitialState. Para tu caso lo haría en componentDidMount. Siga este ejemplo, https://facebook.github.io/react/tips/initial-ajax.html .

También recomendaría leer sobre el ciclo de vida de los componentes con react. https://facebook.github.io/react/docs/component-specs.html .

Reescribir con modificaciones y estado de carga

Personalmente, no me gusta hacer todo, si no, lógica en el renderizado y prefiero usar 'carga' en mi estado y renderizar una fuente impresionante antes de que se cargue el formulario, http://fortawesome.github.io/Font- Impresionante / ejemplos / . Aquí hay una reescritura para mostrarte lo que quiero decir. Si arruiné las garrapatas para cjsx, es porque normalmente solo uso coffeescript como este,.

window.Pages ||= {}

Pages.AwayMessages = React.createClass

  getInitialState: ->
    { loading: true, awayMessage: {} }

  componentDidMount: ->
    App.API.fetchAwayMessage (data) =>
      @setState awayMessage:data.away_message, loading: false

  onToggleCheckbox: (event)->
    @state.awayMessage.master_toggle = event.target.checked
    @setState(awayMessage: @state.awayMessage)

  onTextChange: (event) ->
    @state.awayMessage.text = event.target.value
    @setState(awayMessage: @state.awayMessage)

  onSubmit: (e) ->
    # Not sure what this is for. I'd be careful using globals like this
    window.a = @
    @submitAwayMessage(@state.awayMessage)

  submitAwayMessage: (awayMessage)->
    console.log "I'm saving", awayMessage
    App.API.saveAwayMessage awayMessage, (data) =>
      if data.status == 'ok'
        App.modal.closeModal()
        notificationActions.notify("Away Message saved.")
        @setState awayMessage:awayMessage

  render: ->
    if this.state.loading
      `<i className="fa fa-spinner fa-spin"></i>`
    else
    `<div className="away-messages">
       <div className="header">
         <h4>Away Messages</h4>
       </div>
       <div className="content">
         <div className="input-group">
           <label for="master_toggle">On?</label>
           <input type="checkbox" onChange={this.onToggleCheckbox} checked={this.state.awayMessage.master_toggle} />
         </div>
         <div className="input-group">
           <label for="text">Text</label>
           <input ref="text" onChange={this.onTextChange} value={this.state.awayMessage.text} />
         </div>
       </div>
       <div className="footer">
         <button className="button2" onClick={this.close}>Close</button>
         <button className="button1" onClick={this.onSubmit}>Save</button>
       </div>
     </div>

Eso debería cubrirlo. Ahora bien, esa es una forma de abordar las formas que utilizan estado y valor. También puede usar defaultValue en lugar de value y luego usar refs para obtener los valores cuando envíe. Si sigue esa ruta, le recomendaría que tenga un componente de capa externa (generalmente denominado componentes de alto orden) para obtener los datos y luego pasarlos al formulario como accesorios.

En general, recomendaría leer todos los documentos de reacción y hacer algunos tutoriales. Hay muchos blogs y http://www.egghead.io tiene algunos buenos tutoriales. También tengo algunas cosas en mi sitio, http://www.openmindedinnovations.com .


Solo tengo curiosidad por saber por qué no es bueno hacer llamadas a la API en obtener el estado inicial. getInitialState se ejecuta justo antes de componentDidMount, y la llamada a la API es asincrónica. ¿Es más convencional o hay otra razón detrás de esto?
Mïchael Makaröv

1
No sé exactamente dónde lo leí, pero sé que no pones llamadas de API allí. Hay una biblioteca que se hizo para lidiar con eso, github.com/andreypopp/react-async . Pero no usaría esa biblioteca y simplemente la colocaría en componentDidMount. Sé que en el tutorial sobre la documentación de reacts también se hace la llamada a la API en componentDidMount.
Blaine Hatab

@ MïchaelMakaröv: porque las llamadas a la API son asíncronas y getInitialState devuelve el estado de forma sincrónica. Por lo tanto, su estado inicial se configurará antes de que se complete la llamada a la API.
drogon

2
¿Es seguro reemplazar defaultValue con value? Sé que defaultValue se carga solo en la inicialización, pero el valor también parece hacer esto.
Stealthysnacks

2
@stealthysnacks está bien usar value, pero ahora debe establecer ese valor para que la entrada funcione. defaultValue solo establece el valor inicial y la entrada podrá cambiar, pero cuando se usa el valor ahora está 'controlado'
Blaine Hatab

60

Otra forma de solucionar este problema es cambiando el keyde la entrada.

<input ref="text" key={this.state.awayMessage ? 'notLoadedYet' : 'loaded'} onChange={this.onTextChange} defaultValue={awayMessageText} />

Actualización: dado que esto obtiene votos positivos, tendré que decir que debe tener un objeto de apoyo disabledo readonlymientras se carga el contenido, para que no disminuya la experiencia de ux.

Y sí, lo más probable es que sea un truco, pero hace el trabajo .. ;-)


Implementación ingenua: el enfoque de un campo de entrada cambia mientras se cambia el valor de la clave (salió en KeyUp, por ejemplo)
Arthur Kushman

2
Sí, tiene algunos inconvenientes, pero hace el trabajo fácilmente.
TryingToImprove

Esto es inteligente. Lo usé para selectcon keylo defaultValueque es en realidad value.
Avi

cambiar el keyde la entrada es la clave para obtener el nuevo valor reflejado en la entrada. Lo usé con type="text"éxito.
Jacob Nelson

3

Quizás no sea la mejor solución, pero haría un componente como el siguiente para poder reutilizarlo en todas partes de mi código. Ojalá ya estuviera en reacción por defecto.

<MagicInput type="text" binding={[this, 'awayMessage.text']} />

El componente puede verse así:

window.MagicInput = React.createClass

  onChange: (e) ->
    state = @props.binding[0].state
    changeByArray state, @path(), e.target.value
    @props.binding[0].setState state

  path: ->
    @props.binding[1].split('.')
  getValue: ->
    value = @props.binding[0].state
    path = @path()
    i = 0
    while i < path.length
      value = value[path[i]]
      i++
    value

  render: ->
    type = if @props.type then @props.type else 'input'
    parent_state = @props.binding[0]
    `<input
      type={type}
      onChange={this.onChange}
      value={this.getValue()}
    />`

Donde el cambio por matriz es una función que accede al hash mediante una ruta expresada por una matriz

changeByArray = (hash, array, newValue, idx) ->
  idx = if _.isUndefined(idx) then 0 else idx
  if idx == array.length - 1
    hash[array[idx]] = newValue
  else
    changeByArray hash[array[idx]], array, newValue, ++idx 

0

La forma más confiable de establecer valores iniciales es usar componentDidMount () {} además de render () {}:

... 
componentDidMount(){

    const {nameFirst, nameSecond, checkedStatus} = this.props;

    document.querySelector('.nameFirst').value          = nameFirst;
    document.querySelector('.nameSecond').value         = nameSecond;
    document.querySelector('.checkedStatus').checked    = checkedStatus;        
    return; 
}
...

Puede que le resulte fácil destruir un elemento y reemplazarlo por uno nuevo con

<input defaultValue={this.props.name}/>

Me gusta esto:

if(document.querySelector("#myParentElement")){
    ReactDOM.unmountComponentAtNode(document.querySelector("#myParentElement"));
    ReactDOM.render(
        <MyComponent name={name}  />,
        document.querySelector("#myParentElement")
    );
};

También puede utilizar esta versión del método de desmontaje:

ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(this).parentNode);

5
Estás manipulando el DOM tú mismo aquí ... ¿no es eso un gran NO NO en reaccionar?
Devashish

0

Dar valor al parámetro "placeHolder". Por ejemplo :-

 <input 
    type="text"
    placeHolder="Search product name."
    style={{border:'1px solid #c5c5c5', padding:font*0.005,cursor:'text'}}
    value={this.state.productSearchText}
    onChange={this.handleChangeProductSearchText}
    />
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.