REACT - 将事件处理程序附加到子元素

6

我正在尝试在React中创建一个表单组件,类似于Formik,但更简单。

我考虑的方式是给所有子元素添加一个onChange处理程序。我用children.map()实现了这一点。它可以运行,但是会出现key警告。

Warning: Each child in a list should have a unique "key" prop.

我知道没有办法去抑制这个问题,也许有更好的方法来创建这个表单组件? 此外,当 <input> 不是直接子元素时,应该如何处理?

编辑:我知道如何避免这个问题,我主要想知道最佳的解决方法,包括嵌套输入框的情况。

这是我想要使用它的方式:

<Form>
  <label htmlFor="owner">Owner</label>
  <input
    type="text"
    name="owner"
    id="owner"
  />
  <label htmlFor="description">Description</label>
  <input
    type="text"
    name="description"
    id="description"
  />
  <input
    type="submit"
    value="Submit"
  />
</Form>

这是我的代码:

import React from 'react';
    
class Form extends React.Component {
  constructor(props) {
    super(props);
    this.state = {}
    this.handleInputChange = this.handleInputChange.bind(this);
  }
    
  handleInputChange(event) {
    const target = event.target;
    const value =
      target.type === 'checkbox' ? target.checked : target.value;
    const name = target.name;
    console.log(`${name} : ${value}`)
    this.setState({
      [name]: value
    });
  }
    
  render() {
    return (
      <form>
        {this.props.children.map((child) => {
          if (child.type === "input") {
            return (
              <input
                onChange={this.handleInputChange}
                {...child.props}
              />
            )
          }
        })}
      </form>
    )
  }
}
    
export default Form;

如果您担心输入不是直接子元素,那么您可能需要考虑使用上下文。您无法可靠地遍历组件的子级以下层级,但可以在任何级别注入上下文。 - Wolfie
可能是React.js中理解数组子元素唯一键的重复问题的重复问题。 - Anurag Srivastava
@AnuragSrivastava 我确实知道如何避免警告,但我正在寻求更好的方法来消除问题,并处理嵌套输入。 - Lorenzo
2个回答

4
如果您使用渲染属性,就不会遇到“unique 'key'”属性的问题(这也是Formik的实现方式)。
您的组件很容易设置为将handleChange作为渲染属性传递给其子组件,这也不需要您将input作为直接子组件。
class Form extends Component {
  ...
  handleInputChange() {...}

  render() {
    // Note that we call children as a function,
    // passing `handleChangeInput` as the argument.
    // If you want to pass other other things to the
    // children (handleSubmit, values from state), just
    // add them to the argument you're passing in.
    this.props.children({this.handleInputChange});
  }
}

以下是使用方法:

<Form>
  // Notice that <Form> needs its immediate child to be
  // a function, which has your handler as the argument:
  {({handeInputChange}) => {
    return (
      <form>
        <input type="text" name="owner" onChange={handleInputChange} />
        <input type="checkbox" name="toggle" onChange={handleInputChange} />
        <div>
          // inputs can be nested in other elements
          <input name=“inner” onChange={handleInputChange} />
        <div>
      <form>
    )  
  }}
</Form>

编辑:您在评论中提到,您不想将处理程序显式传递给您的每个输入。实现这一点的另一种方法是使用React上下文,在您的表单中提供一个Provider,并将每个input包装在一个consumer中:

const FormContext = React.createContext();

const FormInput = (props) => {
  const {handleInputChange} = useContext(FormContext);
  return <input handleInputChange={handleInputChange} {...props} />
}

class Form extends Component {
  ...
  handleInputChange() {...}

  render() {
    // Pass anything you want into `value` (state, other handlers),
    // it will be accessible in the consumer 
    <Provider value={{ handleInputChange: this.handleInputChange }}>
      <form>
        {this.props.children}
      </form>
    </Provider>
  }
}

// Usage:
<Form>
  <FormInput type="text" name="owner" />
  <FormInput type="submit" name="submit" />
  <div>
      <FormInput type="checkbox" name="toggle" />
  </div>
</Form>

实际上,Formik也有这个选项,可以使用Field组件或connect函数。

问题在于我想避免在每个输入都有处理程序的样板文件,而只是使用 Form 作为包装组件。 - Lorenzo
有点像 Formik,您只需要提供表单处理程序而不是每个输入。 - Lorenzo
啊,我明白了。你可以使用Context来实现这个,但是你的输入需要被包装在一个context consumer中。我会用这种方法更新我的答案。 - helloitsjoe
1
谢谢,这正是我所需要的。 - Lorenzo

1
我认为这就是您需要的,您可以将子索引作为key添加,因为它们的顺序不会改变,并且在此处reduce不会在数组中返回null,以防子项的类型不是input,map+filter也可以解决这个问题。
class Form extends React.Component {
  constructor(props) {
    super(props);
    this.state = {};
    this.handleInputChange = this.handleInputChange.bind(this);
    // this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleInputChange(event) {
    const target = event.target;
    const value = target.type === "checkbox" ? target.checked : target.value;
    const name = target.name;
    console.log(`${name} : ${value}`);
    this.setState({
      [name]: value
    });
  }

  render() {
    return (
      <form>
        {this.props.children.reduce((childrenAcc, child, index) => {
          if (child.type === "input") {
            return [
              ...childrenAcc,
              <input
                key={index}
                onChange={this.handleInputChange}
                {...child.props}
              />
            ];
          }
          return childrenAcc;
        }, [])}
      </form>
    );
  }
}

function App() {
  return (
    <Form>
      <label htmlFor="owner">Owner</label>
      <input type="text" name="owner" />
      <label htmlFor="description">Description</label>
      <input type="text" name="description" />
      <input type="submit" value="Submit" />
    </Form>
  );
}

请检查此sandbox


确实可以工作,甚至可以使用map(child, index),但是对于嵌套元素的情况不起作用? - Lorenzo
@LefiTarik 是的,不过我认为使用上下文更好,所以那是被接受的答案。我仍然给你点赞,因为这是一个好的、可行的答案。 - Lorenzo
@Lorenzo 是的,我同意这个观点,除非用例很简单。但在其他情况下,使用上下文 API 不够灵活,而这种模式可以解决复杂问题,而不会陷入 RenderProp 地狱(例如,当存在多个提供程序时)。因此,这取决于要解决的问题,我建议您的解决方案以实现简单化。 - Lafi

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