在React ES6中,为什么在输入一个字符后输入框会失去焦点?

235
在我的下面组件中,在键入字符后,输入字段会失去焦点。使用Chrome Inspector时,看起来整个表单在键入时被重新渲染,而不仅仅是输入字段的值属性。
我从eslint和Chrome Inspector都没有收到错误。
提交表单本身正常工作,实际输入字段也正常工作,当它位于渲染的返回值中或作为单独组件导入时,但在下面所编码的方式中却不行。
为什么会这样? 主页面组件
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as actionPost from '../redux/action/actionPost';
import InputText from './form/InputText';
import InputSubmit from './form/InputSubmit';

class _PostSingle extends Component {
    constructor(props, context) {
        super(props, context);
        this.state = {
            post: {
                title: '',
            },
        };
        this.onChange = this.onChange.bind(this);
        this.onSubmit = this.onSubmit.bind(this);
    }
    onChange(event) {
        this.setState({
            post: {
                title: event.target.value,
            },
        });
    }
    onSubmit(event) {
        event.preventDefault();
        this.props.actions.postCreate(this.state.post);
        this.setState({
            post: {
                title: '',
            },
        });
    }
    render() {
        const onChange = this.onChange;
        const onSubmit = this.onSubmit;
        const valueTitle = this.state.post.title;
        const FormPostSingle = () => (
            <form onSubmit={onSubmit}>
                <InputText name="title" label="Title" placeholder="Enter a title" onChange={onChange} value={valueTitle} />
                <InputSubmit name="Save" />
            </form>
        );
        return (
            <main id="main" role="main">
                <div className="container-fluid">
                    <FormPostSingle />
                </div>
            </main>
        );
    }
}

_PostSingle.propTypes = {
    actions: PropTypes.objectOf(PropTypes.func).isRequired,
};

function mapStateToProps(state) {
    return {
        posts: state.posts,
    };
}

function mapDispatchToProps(dispatch) {
    return {
        actions: bindActionCreators(actionPost, dispatch),
    };
}

export default connect(mapStateToProps, mapDispatchToProps)(_PostSingle);

文本输入组件

import React, { PropTypes } from 'react';

const InputText = ({ name, label, placeholder, onChange, value, error }) => {
    const fieldClass = 'form-control input-lg';
    let wrapperClass = 'form-group';
    if (error && error.length > 0) {
        wrapperClass += ' has-error';
    }
    return (
        <div className={wrapperClass}>
            <label htmlFor={name} className="sr-only">{label}</label>
            <input type="text" id={name} name={name} placeholder={placeholder} onChange={onChange} value={value} className={fieldClass} />
            {error &&
                <div className="alert alert-danger">{error}</div>
            }
        </div>
    );
};

InputText.propTypes = {
    name: PropTypes.string.isRequired,
    label: PropTypes.string.isRequired,
    placeholder: PropTypes.string.isRequired,
    onChange: PropTypes.func.isRequired,
    value: PropTypes.string,
    error: PropTypes.string,
};

InputText.defaultProps = {
    value: null,
    error: null,
};

export default InputText;

提交按钮组件

import React, { PropTypes } from 'react';

const InputSubmit = ({ name }) => {
    const fieldClass = 'btn btn-primary btn-lg';
    return (
        <input type="submit" value={name} className={fieldClass} />
    );
};

InputSubmit.propTypes = {
    name: PropTypes.string,
};

InputSubmit.defaultProps = {
    name: 'Submit',
};

export default InputSubmit;

1
感谢这个问题,它是从 https://www.paypal-community.com/t5/Managing-Account/Enter-your-code-page-input-looses-focus/m-p/2876787/highlight/true#M21997 链接过来的,因为这可能与为什么 PayPal 的双重认证开始失去焦点有关... - Tobias Kienzler
34个回答

194

这是因为你在render()函数内部渲染表单。

每当状态/属性发生变化时,该函数都会返回一个新的表单,这会导致你失去焦点。

尝试将函数内部的内容直接放入render中。

<main id="main" role="main">
    <div className="container-fluid">
        <FormPostSingle />
    </div>
</main>
<main id="main" role="main">
    <div className="container-fluid">
        <form onSubmit={onSubmit}>
            <InputText name="title" label="Title" placeholder="Enter a title" onChange={onChange} value={valueTitle} />
            <InputSubmit name="Save" />
        </form>
    </div>
</main>

4
不确定为什么这个回答会被踩,因为它是正确的。引起问题的是匿名函数FormPostSingle,因为它是在render内声明的,每次render运行时都会创建一个带有新函数签名的新函数,这导致React无法跟踪它所创建的元素。 - f.anderzon
6
我猜你是正确的,但为什么不能直接将元素放入其中的原因很可能是因为你要做的整个目的是为了重复使用。如果你想要多个FormPostSingle组件,你会怎么做? - Nateous
10
@Nateous,我的意思是,“不要在渲染函数下创建表单函数”。如果你想重用一个表单组件,应该创建一个单独的函数/组件文件并使用它。 - Alex Yan
5
这个答案很有帮助,我把东西变成了一个函数调用,就像这样 {FormPostSingle(props)},并且它立即有效。我不知道你在说渲染函数方面的内容,因为我使用的是hooks。 - jack blank
44
对于函数组件的开发人员来说,这意味着不要在函数组件主体内定义组件...本质上每个组件都会在模块的顶层(即使您在一个模块中有多个组件)...我曾经因为在函数组件内使用styled-component定义而遇到了这个问题。它看起来像是一个表达式,但实际上它调用了一个使用奇怪语法(对我来说)的styled component 模板字面量语法中的函数。犯了个错! - Henry Mueller
显示剩余8条评论

114

尽管我已经设置了密钥,但这种情况发生了!

原因:

我正在使用文本字段中的一个键。在同一个块内,我有一个输入字段来更新相同文本字段的值。现在,由于组件密钥正在更改,React会重新渲染UI。因此失去焦点。

需要注意的是:

不要使用不断变化的键。


3
哇,从来不知道这可能是问题所在,帮助我解决了我们遇到了一段时间的错误,谢谢。 - Zain Zafar
3
我曾经遇到过类似的情况。我使用nanoid为我的输入组件自动生成了一个键。每次重新渲染都会给它一个新的键。将其更改为在重新渲染时保持不变的键后,它开始正常工作 - 不再失去焦点。 - amota
2
这实际上解决了我的问题。事后看来,我输入的值有一个键,这很愚蠢 :D - Lars
2
我根据输入条件键添加了额外的动态密钥 :-) - 这是我的原因。 - kakabali
1
删除键对我有用,谢谢! - Josh
我之前直接设置了 key={nanoid()}。为了修复这个问题,我只是在我插入列表的新项目中使用了 nanoid()。谢谢你的指点! - Santhosh

76

我曾遇到相同的问题,通过一个快速且简单的方法解决了它:只需使用{compName()}调用组件,而不是使用<compName />

例如,如果我们有:

const foo = ({param1}) => {
   // do your stuff
   return (
      <input type='text' onChange={onChange} value={value} />
   );
};

const main = () => (
   <foo param1={true} />
);

然后,我们只需要改变调用foo()组件的方式:

const main = () => (
   {foo({param1: true})}
);

10
太奇怪和烦人了,谢谢你的提示,这对我也起作用!之前我一直在使用foo(),但把它改为<Foo />后,我不得不改回来才能让我的输入正常工作。 - DavGarcia
这个改变对我也起作用了。不知道原因是什么? - Normal
2
请注意,如果您这样做并且在 foo 中有一个状态变量,每次 foo 的状态更改时,其父组件都将重新渲染。 - Jericho
9
非常感谢。这件事让我很疯狂,所以我不得不去研究为什么会发生这种情况。如果你还有兴趣,@Normal,这里有一个解释:[链接](https://dev.to/igor_bykov/react-calling-functional-components-as-functions-1d3l) - m_wer
该死的命名组件,它们不断作为新组件被创建。 - entropyfeverone

54

发生的情况是这样的:

当你的onChange事件触发时,回调函数会使用新的标题值调用setState,该值作为属性传递给你的文本字段。此时,React会渲染一个新组件,这就是为什么你失去焦点的原因。

我的第一个建议是为你的组件提供key,特别是表单和输入本身。keys允许React通过渲染保留组件的身份。

编辑:

请参阅有关键值的文档:https://reactjs.org/docs/lists-and-keys.html#keys

示例:

    <TextField
      key="password" // <= this is the solution to prevent re-render
      label="eMail"
      value={email}
      variant="outlined"
      onChange={(e) => setEmail(e.target.value)}
    />

这是什么意思,“提供您的组件密钥”?您有任何文档链接吗? - Sterling Bourne
8
正确解释了原因,但关键建议并不能解决问题。只是提醒那些无法弄清楚为什么解决方案对他们无效的人。 - jvhang
11
希望能帮到需要的人,这也可能是因为有一个不必要的键而导致的。我在表格的主体上使用了key={Math.random()},并且该表格具有动态行,这导致了此错误。 - Gavin Mannion
5
具有不断变化的关键属性会导致组件始终重新渲染,而不是仅对更改的部分进行差异比较并重新渲染。 - Juan Lanus
1
@GavinMannion 这对我有用!您能否详细说明一下?为什么人们总是有这行代码? - jiadong
如果您的渲染是动态的,请注意在不同的调用之间保持相同的键。虽然这可能很明显,但有一些示例会使用随机数生成器(例如用于表格行),从而打破上述解决方案,因为不会保留相同的键。 - Alex P.

24

通过添加

autoFocus="autoFocus"

input对我有效

<input
  type="text"
  autoFocus="autoFocus"
  value = {searchString}
  onChange = {handleChange}
/>

13
因为React重新渲染输入元素导致失去焦点,这并不是实际问题,只是掩盖了真正的问题。 - Yihao Gao
14
如果子组件中有多个输入,则此方法无法使用。 - Sujay U N
1
@SujayUN 你是指同时具有“自动聚焦”的多个输入框吗? - floss
3
好的,我会尽力进行翻译。以下是需要翻译的内容:Nop there will be mutiple input fields but need to focus on same which users are typing @flos - Sujay U N
1
这似乎是有效的,还记得将其作为属性传递给组件。 - delimiter

19

您必须为输入组件使用唯一的键。

<input key="random1" type="text" name="displayName" />

key="random1"无法随机生成。 例如,

<div key={uuid()} className='scp-ren-row'>

uuid()函数会在每次重新渲染时生成一组新的字符串,这将导致输入框失去焦点。

如果元素是在.map()函数内生成的,请使用索引作为key的一部分。

{rens.map((ren,i)=>{
    return(
    <div key={`ren${i+1}`} className='scp-ren-row'>
       {ren}{i}
</div>)
}

这将解决问题。


1
我之前使用了 uuid() 生成的随机密钥,但它没有起作用,现在它可以了...谢谢! - Gaëtan Boyals
谢谢,你的解决方案真的解决了我的问题。我正在使用uuidv4生成密钥。 - NINSIIMA WILBER

12

我在使用函数组件中使用styled-components时,遇到了类似的问题。每次输入一个字符后,自定义输入元素都会失去焦点。

经过大量搜索和代码实验,我发现函数组件内的styled-components每次输入一个字符都会重新渲染输入框,因为模板文字语法使表单成为函数,尽管它在Devtools中看起来像是一个表达式。来自@HenryMueller的评论对让我朝着正确方向思考起到了关键作用。

我将样式组件移到函数组件的外部,现在一切都正常工作。

 import React, { useState } from "react";
 import styled from "styled-components";

 const StyledDiv = styled.div`
  margin: 0 auto;
  padding-left: 15px;
  padding-right: 15px;
  width: 100%;
`;

const StyledForm = styled.form`    
  margin: 20px 0 10px;
`;

 const FormInput = styled.input`
  outline: none;
  border: 0;      
  padding: 0 0 15px 0;
  width: 100%;
  height: 50px;
  font-family: inherit;
  font-size: 1.5rem;
  font-weight: 300;
  color: #fff;
  background: transparent;
  -webkit-font-smoothing: antialiased;
`;

const MyForm = () => {
const [value, setValue] = useState<string>("");

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
   setValue(e.target.value);
} 

const handleSubmit = (e: React.FormEvent) => {
  e.preventDefault();
  if(value.trim() === '') {
    return;
  }

  localStorage.setItem(new Date().getTime().toString(), JSON.stringify(value));
  setValue('');
}

 return (
  <StyledDiv>
     <StyledForm onSubmit={handleSubmit}>
        <FormInput type="text" 
        id="inputText" 
        name="inputText"            
        placeholder="What Do You Want To Do Next?"
        value={value}
        onChange={handleChange}/>
    </StyledForm>
   </StyledDiv>
  )
 }

export default MyForm;

在这种情况下,使用styled-components最好的方法是将它们保存在单独的文件中并进行导入。


2
我无法感谢你的足够。这是我的问题,与样式组件有关。我通过将样式组件放在函数外部来解决它。我也可以把它放在单独的文件中。非常感谢您发布这个。 - sandy
谢谢!这也是我在将数据分发到Redux中遇到的问题。 - Harvester Haidar

12

我也遇到了这个问题,我的问题与使用另一个组件包装textarea有关。

// example with this problem
import React from 'react'

const InputMulti = (props) => {
  const Label = ({ label, children }) => (
    <div>
      <label>{label}</label>
      { children }
    </div>
  )

  return (
    <Label label={props.label}>
      <textarea
        value={props.value}
        onChange={e => props.onChange(e.target.value)}
      />
    </Label>
  )
}

export default InputMulti
当状态改变时,React会呈现InputMulti组件,这意味着每次都会重新定义Label组件,尽管输出在结构上相同,但由于JS的原因,函数将被视为不等于。
我的解决方案是将Label组件移出InputMulti组件,使其保持静态。
// fixed example
import React from 'react'

const Label = ({ label, children }) => (
  <div>
    <label>{label}</label>
    { children }
  </div>
)

const InputMulti = (props) => {
  return (
    <Label label={props.label}>
      <textarea
        value={props.value}
        onChange={e => props.onChange(e.target.value)}
      />
    </Label>
  )
}

export default InputMulti

我注意到人们经常将本地使用的组件放在想要使用它的组件内部。通常是为了利用函数作用域并获得父组件的props访问权限。

const ParentComp = ({ children, scopedValue }) => {
  const ScopedComp = () => (<div>{ scopedValue }</div>)
  return <ScopedComp />
}

我从未真正考虑过为什么需要这样做,因为您可以将道具钻入内部函数并从父级组件外部化它。

这个问题是为什么您应该始终将组件彼此分离的完美例子,即使它们在一个模块中使用。此外,您还可以使用智能文件夹结构来保持东西靠近。

src/
  components/
    ParentComp/
      ParentComp.js
      components/
        ScopedComp.js

刚刚花了很长时间尝试解决这个问题,把你的答案当作无关紧要的东西给忽略了,然后才意识到它和我的一样!!谢谢!!!! - schoon

3

我的问题是在同一个文件的无状态组件中进行了重新渲染。因此,一旦我摆脱了这个不必要的无状态组件,直接将代码放入其中,就不会出现不必要的重新渲染。

render(){
   const NewSocialPost = () => 
       <div className='new-post'>
           <input
                onChange={(e) => this.setState({ newSocialPost: e.target.value })}
                value={this.state.newSocialPost}/>
           <button onClick={() => this._handleNewSocialPost()}>Submit</button>
      </div>

return (
            <div id='social-post-page'>
                <div className='post-column'>
                    <div className='posts'>
                        <Stuff />
                    </div>
                    <NewSocialPost />
                </div>
                <MoreStuff />
            </div>

但是如果我们有200个<NewSocialPost>组件要返回,但不想重复200次<div className='new-post'>代码,该怎么办? - Sterling Bourne
1
@SterlingBourne 我认为你想要改变父组件的生命周期和映射代码,所以类似{NewSocialPosts.map(SocialPost => <NewSocialPost SocialPost={SocialPost} /> )}这样的东西。 - Kevin Danikowski

3

解决方案 -

  1. 为输入元素添加一个unique键,因为它有助于React识别哪个项目已更改(Reconciliation)。确保您的键不会更改,它必须是constantunique

  2. 如果您在React组件内定义了一个styled component,并且您的输入元素位于该styled component中,则将该styled component定义在输入组件之外。否则,在主组件的每个状态更改时,它都会重新呈现您的styled component和输入,这将导致失去焦点。

import React, { useState } from "react";
import styled from "styled-components";

const Container = styled.div`
  padding: 1rem 0.5rem;
  border: 1px solid #000;
`;

function ExampleComponent() {
  // Container styled component should not be inside this ExampleComponent
  const [userName, setUserName] = useState("");

  const handleInputChange = event => {
    setUserName(event.target.value);
  };

  return (
    <React.Fragment>
      <Container> {/* Styled component */}
        <input
          key="user_name_key" // Unique and constant key
          type="text"
          value={userName}
          onChange={handleInputChange}
        />
      </Container>
    </React.Fragment>
  );
}

export default ExampleComponent;


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接