React.js: entrada que pierde el enfoque al volver a renderizar

97

Solo estoy escribiendo en la entrada de texto y, en onChangecaso de que llame setState, React vuelve a reproducir mi interfaz de usuario. El problema es que la entrada de texto siempre pierde el enfoque, por lo que necesito enfocarlo nuevamente para cada letra: D.

var EditorContainer = React.createClass({

    componentDidMount: function () {
        $(this.getDOMNode()).slimScroll({height: this.props.height, distance: '4px', size: '8px'});
    },

    componentDidUpdate: function () {
        console.log("zde");
        $(this.getDOMNode()).slimScroll({destroy: true}).slimScroll({height: 'auto', distance: '4px', size: '8px'});
    },

    changeSelectedComponentName: function (e) {
        //this.props.editor.selectedComponent.name = $(e.target).val();
        this.props.editor.forceUpdate();
    },

    render: function () {

        var style = {
            height: this.props.height + 'px'
        };
        return (
            <div className="container" style={style}>
                <div className="row">
                    <div className="col-xs-6">
                    {this.props.selected ? <h3>{this.props.selected.name}</h3> : ''}
                    {this.props.selected ? <input type="text" value={this.props.selected.name} onChange={this.changeSelectedComponentName} /> : ''}
                    </div>
                    <div className="col-xs-6">
                        <ComponentTree editor={this.props.editor} components={this.props.components}/>
                    </div>
                </div>
            </div>
        );
    }

});
Krab
fuente
La única razón por la que eso sucedería es si a) algo más roba el foco, ob) la entrada está parpadeando. ¿Podría proporcionar un jsfiddle / jsbin que muestre el problema? Aquí hay una base de reacción jsbin .
Brigand
lol ... bueno, eso suena un poco molesto: P Con jquery, establecería un identificador para el nuevo archivo de entrada renderizado y luego llamaría a focus sobre él. No estoy seguro de cómo se vería el código en js simple. Pero estoy seguro de que puedes hacerlo :)
Medda86
1
@FakeRainBrigand: ¿a qué te refieres con parpadeo?
Krab
@Krab, como si this.props.selected se volviera falso y luego volviera a ser verdadero. Eso haría que la entrada se desmontara y luego se volviera a montar.
Brigand
@Krab, intente eliminar las líneas slimScroll; que podría estar haciendo algo extraño que está causando problemas.
Brigand

Respuestas:

87

Sin ver el resto de su código, esto es una suposición. Cuando cree un EditorContainer, especifique una clave única para el componente:

<EditorContainer key="editor1"/>

Cuando ocurre una re-renderización, si se ve la misma clave, esto le dirá a React que no golpee y regenere la vista, en su lugar la reutilice. Entonces, el elemento enfocado debe mantener el enfoque.

z5h
fuente
2
Esto resolvió mi problema con la representación de un subcomponente que contiene un formulario de entrada. Pero en mi caso necesitaba lo contrario: el formulario no se volvía a renderizar cuando yo quería. Agregar el atributo clave funcionó.
Sam Texas
2
agregar clave a la entrada no ayudó
Mïchael Makaröv
6
Quizás los componentes que contienen su entrada también se vuelvan a representar. Revise las claves de los componentes envolventes.
Petr Gladkikh
Voto comentario de @PetrGladkikh. Necesitaba llaves en todos los componentes adjuntos.
Brian Cragun
49

Sigo volviendo aquí una y otra vez y siempre encuentro la solución para mi otro lugar al final. Entonces, lo documentaré aquí porque sé que lo olvidaré nuevamente.

La razón inputfue perder el enfoque en mi caso se debió al hecho de que estaba volviendo a representar el inputcambio de estado.

Código de buggy:

import React from 'react';
import styled from 'styled-components';

class SuperAwesomeComp extends React.Component {
  state = {
    email: ''
  };
  updateEmail = e => {
    e.preventDefault();
    this.setState({ email: e.target.value });
  };
  render() {
    const Container = styled.div``;
    const Input = styled.input``;
    return (
      <Container>
        <Input
          type="text"
          placeholder="Gimme your email!"
          onChange={this.updateEmail}
          value={this.state.email}
        />
      </Container>
    )
  }
}

Entonces, el problema es que siempre comienzo a codificar todo en un solo lugar para probar rápidamente y luego dividirlo todo en módulos separados. Pero, aquí esta estrategia fracasa porque la actualización del estado en el cambio de entrada desencadena la función de renderizado y se pierde el enfoque.

La solución es simple, realice la modularización desde el principio, en otras palabras, "Mueva el Inputcomponente fuera de renderfunción"

Código fijo

import React from 'react';
import styled from 'styled-components';

const Container = styled.div``;
const Input = styled.input``;

class SuperAwesomeComp extends React.Component {
  state = {
    email: ''
  };
  updateEmail = e => {
    e.preventDefault();
    this.setState({ email: e.target.value });
  };
  render() {
    return (
      <Container>
        <Input
          type="text"
          placeholder="Gimme your email!"
          onChange={this.updateEmail}
          value={this.state.email}
        />
      </Container>
    )
  }
}

Árbitro. a la solución: https://github.com/styled-components/styled-components/issues/540#issuecomment-283664947

Deepak
fuente
1
Estaba usando un HOC dentro de la función de renderizado, mover la llamada HOC a la definición del componente funcionó. De alguna manera similar a esta respuesta.
Mark E
3
Creo que este es mi problema, pero tengo problemas para mover los componentes fuera de render()y class ... extends Componentdebido a la referencia a this. ponChange={this.handleInputChange}
Nateous
3
La moral de la historia, para los componentes de la clase React, no define componentes dentro de la renderfunción, para los componentes funcionales de React, no define componentes en el cuerpo de la función.
Wong Jia Hau
1
¡Gracias! ¡¡Me salvaste el trasero !!
K-Sato
1
@Nateous tuve exactamente la misma situación. Llamadas HOC dentro de la renderfunción a las que se les estaba pasando el método 'this.handleChange' y devolvían los componentes que se utilizarían. Ej const TextAreaDataField = TextAreaDataFieldFactory(this.handleChange). Simplemente moví la creación del componente al constructor y accedo a ellos en el rendermétodo como this.TextAreaDataField. Trabajado como un encanto.
Marcus Junius Brutus
36

Si es un problema dentro de un enrutador de reacción, <Route/>use el renderaccesorio en lugar decomponent .

<Route path="/user" render={() => <UserPage/>} />


La pérdida de enfoque ocurre porque el componentaccesorio usa React.createElementcada vez en lugar de simplemente volver a renderizar los cambios.

Detalles aquí: https://reacttraining.com/react-router/web/api/Route/component

spencer.sm
fuente
2
Este fue esencialmente mi problema. Específicamente, estaba pasando una función anónima al componente prop, lo que provocó la pérdida de enfoque:, <Route component={() => <MyComponent/>} />cuando debería haberlo hecho<Route component={MyComponent} />
Daniel
Me salvaste el día, ¡así de simple!
Fabricio
12

Mi respuesta es similar a lo que dijo @ z5h.

En mi caso, solía Math.random()generar un único keypara el componente.

Pensé keyque solo se usa para activar una repetición para ese componente en particular en lugar de volver a renderizar todos los componentes de esa matriz (devuelvo una matriz de componentes en mi código). No sabía que se usa para restaurar el estado después de la rendición.

Quitar eso hizo el trabajo por mí.

Shankar Thyagarajan
fuente
1
Un módulo como npmjs.com/package/shortid también es una buena forma de manejar algo como esto.
skwidbreth
4

La aplicación del autoFocusatributo al inputelemento puede funcionar como una solución alternativa en situaciones en las que solo hay una entrada que debe enfocarse. En ese caso, un keyatributo sería innecesario porque es solo un elemento y, además, no tendría que preocuparse por dividir el inputelemento en su propio componente para evitar perder el foco en la reproducción del componente principal.

spakmad
fuente
4

Tengo el mismo comportamiento.

El problema en mi código fue que creé una matriz anidada de elementos jsx como este:

const example = [
            [
                <input value={'Test 1'}/>,
                <div>Test 2</div>,
                <div>Test 3</div>,
            ]
        ]

...

render = () => {
    return <div>{ example }</div>
}

Cada elemento de esta matriz anidada se vuelve a renderizar cada vez que actualizo el elemento principal. Y así las entradas pierden su prop "ref" cada vez

Solucioné el problema con transformar la matriz interna en un componente de reacción (una función con una función de renderizado)

const example = [
            <myComponentArray />
        ]

 ...

render = () => {
    return <div>{ example }</div>
}

EDITAR:

El mismo problema aparece cuando construyo un anidado React.Fragment

const SomeComponent = (props) => (
    <React.Fragment>
        <label ... />
        <input ... />
    </React.Fragment>
);

const ParentComponent = (props) => (
    <React.Fragment>
        <SomeComponent ... />
        <div />
    </React.Fragment>
);
Chrisheyn
fuente
3

Me encontré con este problema y vine aquí en busca de ayuda. ¡Comprueba tu CSS! El campo de entrada no puede tener user-select: none;o no funcionará en un iPad.

David Sinclair
fuente
3

Resolví el mismo problema eliminando el atributo clave en la entrada y sus elementos principales

// Before
<input
    className='invoice_table-input invoice_table-input-sm'
    type='number'
    key={ Math.random }
    defaultValue={pageIndex + 1}
    onChange={e => {
        const page = e.target.value ? Number(e.target.value) - 1 : 0
        gotoPage(page)
    }}
/>
// After
<input
    className='invoice_table-input invoice_table-input-sm'
    type='number'
    defaultValue={pageIndex + 1}
    onChange={e => {
        const page = e.target.value ? Number(e.target.value) - 1 : 0
        gotoPage(page)
    }}
/>

Adrián Pedreira
fuente
2

Para mí, esto se debía a que el cuadro de entrada de búsqueda se mostraba en el mismo componente (llamado UserList) que la lista de resultados de búsqueda. Entonces, cada vez que cambiaban los resultados de la búsqueda, se volvía a representar todo el componente UserList, incluido el cuadro de entrada.

Mi solución fue crear un componente completamente nuevo llamado UserListSearch que está separado de UserList . No necesité establecer claves en los campos de entrada en UserListSearch para que esto funcione. La función de renderizado de mi UsersContainer ahora se ve así:

class UserContainer extends React.Component {
  render() {
    return (
      <div>
        <Route
          exact
          path={this.props.match.url}
          render={() => (
            <div>
              <UserListSearch
                handleSearchChange={this.handleSearchChange}
                searchTerm={this.state.searchTerm}
              />
              <UserList
                isLoading={this.state.isLoading}
                users={this.props.users}
                user={this.state.user}
                handleNewUserClick={this.handleNewUserClick}
              />
            </div>
          )}
        />
      </div>  
    )
  }
}

Espero que esto también ayude a alguien.

Dom edén
fuente
2

Si el campo de entrada está dentro de otro elemento (es decir, un elemento contenedor como <div key={"bart"}...><input key={"lisa"}...> ... </input></div>, las elipses aquí indican código omitido), debe haber una clave única y constante en el elemento contenedor (así como en el campo de entrada) . De lo contrario, React presenta un nuevo elemento contenedor cuando se actualiza el estado del niño en lugar de simplemente volver a representar el contenedor anterior. Lógicamente, solo se debería actualizar el elemento hijo, pero ...

Tuve este problema al intentar escribir un componente que tomaba mucha información de dirección. El código de trabajo se ve así

// import react, components
import React, { Component } from 'react'

// import various functions
import uuid from "uuid";

// import styles
import "../styles/signUp.css";

export default class Address extends Component {
  constructor(props) {
    super(props);
    this.state = {
      address1: "",
      address2: "",
      address1Key: uuid.v4(),
      address2Key: uuid.v4(),
      address1HolderKey: uuid.v4(),
      address2HolderKey: uuid.v4(),
      // omitting state information for additional address fields for brevity
    };
    this.handleChange = this.handleChange.bind(this);
  }

  handleChange(event) {
    event.preventDefault();
    this.setState({ [`${event.target.id}`]: event.target.value })
  }

  render() {
    return (
      <fieldset>
        <div className="labelAndField" key={this.state.address1HolderKey} >
          <label className="labelStyle" for="address1">{"Address"}</label>
          <input className="inputStyle"
            id="address1"
            name="address1"
            type="text"
            label="address1"
            placeholder=""
            value={this.state.address1}
            onChange={this.handleChange}
            key={this.state.address1Key} ></input >
        </div> 
        <div className="labelAndField" key={this.state.address2HolderKey} >
          <label className="labelStyle" for="address2">{"Address (Cont.)"}</label>
          <input className="inputStyle"
            id="address2"
            name="address2"
            type="text"
            label="address2"
            placeholder=""
            key={this.state.address2Key} ></input >
        </div>
        {/* omitting rest of address fields for brevity */}
      </fieldset>
    )
  }
}

Los lectores perspicaces notarán que <fieldset>es un elemento contenedor, pero no requiere una clave. Lo mismo vale para <>e <React.Fragment>incluso <div>¿Por qué? Quizás solo el contenedor inmediato necesite una llave. No se. Como dicen los libros de texto de matemáticas, la explicación se deja al lector como un ejercicio.

Charles Goodwin
fuente
1

La razón principal es: cuando React vuelva a renderizar, su referencia DOM anterior no será válida. Significa que react ha cambiado el árbol DOM, y this.refs.input.focus no funcionará, porque la entrada aquí ya no existe.

hlissnake
fuente
1

Las respuestas proporcionadas no me ayudaron, esto fue lo que hice, pero tuve una situación única.

Para limpiar el código, tiendo a usar este formato hasta que esté listo para colocar el componente en otro archivo.

render(){
   const MyInput = () => {
      return <input onChange={(e)=>this.setState({text: e.target.value}) />
   }
   return(
      <div>
         <MyInput />
      </div>
   )

Pero esto hizo que perdiera el foco, cuando puse el código directamente en el div, funcionó.

return(
   <div>
      <input onChange={(e)=>this.setState({text: e.target.value}) />
   </div>
)

No sé por qué es esto, este es el único problema que he tenido al escribirlo de esta manera y lo hago en la mayoría de los archivos que tengo, pero si alguien hace algo similar, es por eso que pierde el enfoque.

Kevin Danikowski
fuente
1

Tuve el mismo problema con una tabla html en la que tengo líneas de texto de entrada en una columna. dentro de un bucle leo un objeto json y creo filas en particular, tengo una columna con inputtext.

http://reactkungfu.com/2015/09/react-js-loses-input-focus-on-typing/

Logré solucionarlo de la siguiente manera

import { InputTextComponent } from './InputTextComponent';
//import my  inputTextComponent 
...

var trElementList = (function (list, tableComponent) {

    var trList = [],
        trElement = undefined,
        trElementCreator = trElementCreator,
        employeeElement = undefined;



    // iterating through employee list and
    // creating row for each employee
    for (var x = 0; x < list.length; x++) {

        employeeElement = list[x];

        var trNomeImpatto = React.createElement('tr', null, <td rowSpan="4"><strong>{employeeElement['NomeTipologiaImpatto'].toUpperCase()}</strong></td>);
        trList.push(trNomeImpatto);

        trList.push(trElementCreator(employeeElement, 0, x));
        trList.push(trElementCreator(employeeElement, 1, x));
        trList.push(trElementCreator(employeeElement, 2, x));

    } // end of for  

    return trList; // returns row list

    function trElementCreator(obj, field, index) {
        var tdList = [],
            tdElement = undefined;

        //my input text
        var inputTextarea = <InputTextComponent
            idImpatto={obj['TipologiaImpattoId']}//index
            value={obj[columns[field].nota]}//initial value of the input I read from my json data source
            noteType={columns[field].nota}
            impattiComposite={tableComponent.state.impattiComposite}
            //updateImpactCompositeNote={tableComponent.updateImpactCompositeNote}
        />

        tdElement = React.createElement('td', { style: null }, inputTextarea);
        tdList.push(tdElement);


        var trComponent = createClass({

            render: function () {
                return React.createElement('tr', null, tdList);
            }
        });
        return React.createElement(trComponent);
    } // end of trElementCreator

});
...    
    //my tableComponent
    var tableComponent = createClass({
        // initial component states will be here
        // initialize values
        getInitialState: function () {
            return {
                impattiComposite: [],
                serviceId: window.sessionStorage.getItem('serviceId'),
                serviceName: window.sessionStorage.getItem('serviceName'),
                form_data: [],
                successCreation: null,
            };
        },

        //read a json data soure of the web api url
        componentDidMount: function () {
            this.serverRequest =
                $.ajax({
                    url: Url,
                    type: 'GET',
                    contentType: 'application/json',
                    data: JSON.stringify({ id: this.state.serviceId }),
                    cache: false,
                    success: function (response) {
                        this.setState({ impattiComposite: response.data });
                    }.bind(this),

                    error: function (xhr, resp, text) {
                        // show error to console
                        console.error('Error', xhr, resp, text)
                        alert(xhr, resp, text);
                    }
                });
        },

        render: function () {
    ...
    React.createElement('table', {style:null}, React.createElement('tbody', null,trElementList(this.state.impattiComposite, this),))
    ...
    }



            //my input text
            var inputTextarea = <InputTextComponent
                        idImpatto={obj['TipologiaImpattoId']}//index
                        value={obj[columns[field].nota]}//initial value of the input I read //from my json data source
                        noteType={columns[field].nota}
                        impattiComposite={tableComponent.state.impattiComposite}//impattiComposite  = my json data source

                    />//end my input text

                    tdElement = React.createElement('td', { style: null }, inputTextarea);
                    tdList.push(tdElement);//add a component

        //./InputTextComponent.js
        import React from 'react';

        export class InputTextComponent extends React.Component {
          constructor(props) {
            super(props);
            this.state = {
              idImpatto: props.idImpatto,
              value: props.value,
              noteType: props.noteType,
              _impattiComposite: props.impattiComposite,
            };
            this.updateNote = this.updateNote.bind(this);
          }

        //Update a inpute text with new value insert of the user

          updateNote(event) {
            this.setState({ value: event.target.value });//update a state of the local componet inputText
            var impattiComposite = this.state._impattiComposite;
            var index = this.state.idImpatto - 1;
            var impatto = impattiComposite[index];
            impatto[this.state.noteType] = event.target.value;
            this.setState({ _impattiComposite: impattiComposite });//update of the state of the father component (tableComponet)

          }

          render() {
            return (
              <input
                className="Form-input"
                type='text'
                value={this.state.value}
                onChange={this.updateNote}>
              </input>
            );
          }
        }
Fábrica de software
fuente
0

Resulta que estaba vinculando thisel componente que estaba causando que se volviera a rendir.

Pensé que lo publicaría aquí en caso de que alguien más tuviera este problema.

Tuve que cambiar

<Field
    label="Post Content"
    name="text"
    component={this.renderField.bind(this)}
/>

A

<Field
    label="Post Content"
    name="text"
    component={this.renderField}
/>

Solución simple ya que en mi caso, no se necesita realmente thisen renderField, pero espero que me publicar esto ayudará a alguien más.

Matt D
fuente
0

Cambié valueprop a defaultValue. Funciona para mi.

...
// before
<input value={myVar} />

// after
<input defaultValue={myVar} />
Chrisheyn
fuente
0

Mi problema fue que nombré mi clave dinámicamente con un valor del elemento, en mi caso "nombre", por lo que la clave era clave = { ${item.name}-${index}}. Entonces, cuando quería cambiar la entrada con item.name como valor, la clave también cambiaría y, por lo tanto, reaccionaría no reconocería ese elemento

Amogu
fuente
0

Tuve este problema y el problema resultó ser que estaba usando un componente funcional y me estaba vinculando con el estado de un componente principal. Si cambiaba a usar un componente de clase, el problema desaparecía. Con suerte, hay una forma de evitar esto cuando se utilizan componentes funcionales, ya que es mucho más conveniente para los procesadores de elementos simples y otros.

Kyle Crossman
fuente
0

incluyó el siguiente código en la entrada de la etiqueta:

ref={(input) => {
     if (input) {
         input.focus();
     }
 }}

Antes de:

<input
      defaultValue={email}
      className="form-control"
      type="email"
      id="email"
      name="email"
      placeholder={"[email protected]"}
      maxLength="15"
      onChange={(e) => validEmail(e.target.value)}
/>

Después:

<input
     ref={(input) => {
          if (input) {
             input.focus();
          }
      }}
      defaultValue={email}
      className="form-control"
      type="email"
      id="email"
      name="email"
      placeholder={"[email protected]"}
      maxLength="15"
      onChange={(e) => validEmail(e.target.value)}
/>
Thiago Pires
fuente