大型表单上的双向数据绑定

Two-way data binding on large forms

(完成 React.js 教程后,我目前正在编写我的第一个小应用程序以获得更多练习。所以这将是一个新手问题,答案肯定在某处,但显然我不知道要搜索什么。)

Google列举了很多关于如何实现一个输入框的双向数据绑定的例子。但是大型、复杂的表单又如何呢?可能还可以选择动态添加?

假设我的表单由水平行的输入字段组成。所有行都相同:名字、姓氏、出生日期等。在 table 的底部有一个按钮可以插入一个新的这样的行。所有这些数据都存储在一个数组中。如何将每个输入字段绑定到其各自的数组元素,以便在用户编辑值时更新数组?

每行两列的工作示例:

import { useState} from 'react';

function App() {
    var [name1, setName1] = useState('Alice');
    var [score1, setScore1] = useState('100');
    var [name2, setName2] = useState('Bob');
    var [score2, setScore2] = useState('200');

    function changeNameHandler1 (e) {
        console.log(e)
        setName1(e.target.value)
    }

    function changeScoreHandler1 (e) {
        setScore1(e.target.value)
    }

    function changeNameHandler2 (e) {
        setName2(e.target.value)
    }

    function changeScoreHandler2 (e) {
        setScore2(e.target.value)
    }

    return (
        <div>
            <table>
                <tbody>
                    <tr>
                        <td><input name="name1" id="id1" type="text" value={name1} onChange={changeNameHandler1} /></td>
                        <td><input name="score1" type="text" value={score1} onChange={changeScoreHandler1} /></td>
                    </tr>
                    <tr>
                        <td><input name="name2" type="text" value={name2} onChange={changeNameHandler2} /></td>
                        <td><input name="score2" type="text" value={score2} onChange={changeScoreHandler2} /></td>
                    </tr>
                </tbody>
            </table>
            {name1} has a score of {score1}<br />
            {name2} has a score of {score2}<br />
        </div>
    );
}

export default App;

如何在不必为数百个字段单独添加处理函数的情况下扩大规模?

这个问题有多种解决方案,例如:

解决方案#1 使用 useRef 存储字段的值

import { useRef, useCallback } from "react";

export default function App() {
  const fullNameInputElement = useRef();
  const emailInputElement = useRef();
  const passwordInputElement = useRef();
  const passwordConfirmationInputElement = useRef();

  const formHandler = useCallback(
    () => (event) => {
      event.preventDefault();

      const data = {
        fullName: fullNameInputElement.current?.value,
        email: emailInputElement.current?.value,
        password: passwordInputElement.current?.value,
        passwordConfirmation: passwordConfirmationInputElement.current?.value
      };

      console.log(data);
    },
    []
  );

  return (
    <form onSubmit={formHandler()}>
      <label htmlFor="full_name">Full name</label>
      <input
        ref={fullNameInputElement}
        id="full_name"
        placeholder="Full name"
        type="text"
      />

      <label htmlFor="email">Email</label>
      <input
        ref={emailInputElement}
        id="email"
        placeholder="Email"
        type="email"
      />

      <label htmlFor="password">Password</label>
      <input
        ref={passwordInputElement}
        id="password"
        placeholder="Password"
        type="password"
      />

      <label htmlFor="password_confirmation">Password Confirmation</label>
      <input
        ref={passwordConfirmationInputElement}
        id="password_confirmation"
        placeholder="Password Confirmation"
        type="password"
      />

      <button type="submit">Submit</button>
    </form>
  );
}

或者仍然使用 useState 但将所有值存储在一个对象中

import { useState, useCallback } from "react";

const initialUserData = {
  fullName: "",
  email: "",
  password: "",
  passwordConfirmation: ""
};

export default function App() {
  const [userData, setUserData] = useState(initialUserData);

  const updateUserDataHandler = useCallback(
    (type) => (event) => {
      setUserData({ ...userData, [type]: event.target.value });
    },
    [userData]
  );

  const formHandler = useCallback(
    () => (event) => {
      event.preventDefault();

      console.log(userData);
    },
    [userData]
  );

  return (
    <form onSubmit={formHandler()}>
      <label htmlFor="full_name">Full name</label>
      <input
        id="full_name"
        placeholder="Full name"
        type="text"
        value={userData.fullName}
        onChange={updateUserDataHandler("fullName")}
      />
      <label>Email</label>
      <input
        id="email"
        placeholder="Email"
        type="email"
        value={userData.email}
        onChange={updateUserDataHandler("email")}
      />
      <label htmlFor="password">Password</label>
      <input
        id="password"
        placeholder="Password"
        type="password"
        value={userData.password}
        onChange={updateUserDataHandler("password")}
      />
      <label htmlFor="password_confirmation">Password Confirmation</label>
      <input
        id="password_confirmation"
        placeholder="Password Confirmation"
        type="password"
        value={userData.passwordConfirmation}
        onChange={updateUserDataHandler("passwordConfirmation")}
      />
      <button type="submit">Submit</button>
    </form>
  );
}

解决方案#2 或者您有多个库也提供 react-form-hook https://react-hook-form.com/

等解决方案

您仍然可以将字段存储在一个对象中,然后在您想要添加字段时添加到该对象中。然后通过按键映射显示出来

简单示例:

import { useState } from 'react'

const App = () => {
  const [fields, setFields ] = useState({
    field_0: ''
  })

  const handleChange = (e) => {
    setFields({
      ...fields,
      [e.target.name]: e.target.value
    })
  }

  const addField = () => setFields({
    ...fields,
    ['field_' + Object.keys(fields).length]: ''
  })

  const removeField = (key) => {
    delete fields[key]
    setFields({...fields})
  }

  return (
    <div>
      {Object.keys(fields).map(key => (
        <div>
          <input onChange={handleChange} key={key} name={key} value={fields[key]} />
          <button onClick={() => removeField(key)}>Remove Field</button>
        </div>
      ))}

      <button onClick={() => addField()}>Add Field</button>
      <button onClick={() => console.log(fields)}>Log fields</button>
    </div>
  );
};

export default App;

以下是我认为您在问题中试图实现的目标:

import { useState } from 'react'

const App = () => {
  const [fieldIndex, setFieldIndex] = useState(1)
  const [fields, setFields ] = useState({
    group_0: {
      name: '',
      score: ''
    }
  })

  const handleChange = (e, key) => {
    setFields({
      ...fields,
      [key]: {
        ...fields[key],
        [e.target.name]: e.target.value
      }
    })
  }

  const addField = () => {
    setFields({
      ...fields,
      ['group_' + fieldIndex]: {
        name: '',
        score: ''
      }
    })
    setFieldIndex(i => i + 1)
  }

  const removeField = (key) => {
    delete fields[key]
    setFields({...fields})
  }

  return (
    <div>
      {Object.keys(fields).map((key, index) => (
        <div key={key}>
          <div>Group: {index}</div>
          <label>Name:</label>
          <input onChange={(e) => handleChange(e, key)} name='name' value={fields[key].name} />
          <label>Score: </label>
          <input onChange={(e) => handleChange(e, key)} name='score' value={fields[key].score} />
          <button onClick={() => removeField(key)}>Remove Field Group</button>
        </div>
      ))}

      <button onClick={() => addField()}>Add Field</button>
      <button onClick={() => console.log(fields)}>Log fields</button>
    </div>
  );
};

export default App;

您可能希望保留用于命名的索引,在这种情况下您可以使用数组。然后你只需传递索引来改变你的输入。下面是一个使用数组的例子:

import { useState } from 'react'

const App = () => {
  const [fields, setFields ] = useState([
    {
      name: '',
      score: ''
    }
  ])

  const handleChange = (e, index) => {
    fields[index][e.target.name] = e.target.value
    setFields([...fields])
  }

  const addField = () => {
    setFields([
      ...fields,
        {
          name: '',
          score: ''
        }
    ])
  }

  const removeField = (index) => {
    fields.splice(index, 1)
    setFields([...fields])
  }

  return (
    <div>
      {fields.map((field, index) => (
        <div key={index}>
          <div>Group: {index}</div>
          <label>Name:</label>
          <input onChange={(e) => handleChange(e, index)} name='name' value={field.name} />
          <label>Score: </label>
          <input onChange={(e) => handleChange(e, index)} name='score' value={field.score} />
          <button onClick={() => removeField(index)}>Remove Field Group</button>
        </div>
      ))}

      <button onClick={() => addField()}>Add Field</button>
      <button onClick={() => console.log(fields)}>Log fields</button>
    </div>
  );
};

export default App;