在 React 中填写复杂表单时设置对象属性的最佳方法是什么

What is the best way to set object properties when filling a complex form in React

我是 React 新手。我有一个看似标准的任务:填写有关用户的表单数据并将其发送到服务器。表单是一组组件,如:基本信息、护照数据、兴趣爱好等。+保存按钮。

我是按照下面的方法做的。有一个 class 描述模型。创建组件时,我在 useRef 中创建了此 class 的实例。此外,我通过它们的道具将这个 model 变量传递给所有子组件。模型属性填充在组件中。因此,当我单击 Save 按钮时,我已经填充了模型属性。Here's an example.

请问,这种填充复杂对象数据的方法好吗?也许换一种方式更好?有什么最佳实践吗?也许我应该使用 redux 来完成这项任务?

model.ts

 class Model {
      // Component 1
      firstName: string;
      lastName: string;
    
      // Component 2
      passport: string;
      address: string;
    }
    
    interface IComponent {
      model: Model;
    }
    
    export { Model, IComponent };

index.tsx

export const App: React.FunctionComponent<{}> = () =>
{
 const model = useRef<Model>(new Model());

 const save = () =>{
   console.log(model.current);
 }

return (
  <React.Fragment>
    <Component1 model={model.current} />
    <Component2 model={model.current} />
    <button onClick={save}>Сохранить</button>
  </React.Fragment>
);
}
render(<App />, document.getElementById('root'));

Component1.tsx

export const Component1: React.FunctionComponent<IComponent> = ({ model }) => {

  const [firstNameValue, setFirstNameValue] = useState(model.firstName);
  const [lastNameValue, setLastNameValue] = useState(model.lastName);

  const changeFirstName = (e: React.ChangeEvent<HTMLInputElement>) => {
      model.firstName = e.target.value;
      setFirstNameValue(e.target.value);
  }

  const changeLastName = (e: React.ChangeEvent<HTMLInputElement>) => {
      model.lastName = e.target.value;
      setLastNameValue(e.target.value);
  }

  return (
  <React.Fragment>
   <div>
    <label htmlFor="firstName">FirstName:</label>
    <input name="firstName" value={firstNameValue} onChange={changeFirstName} />
   </div>
   <div>
    <label htmlFor="lastName">LastName:</label>
    <input name="lastName" value={lastNameValue} onChange={changeLastName}/>
   </div>
  </React.Fragment>);
};

Component2.tsx

export const Component2: React.FunctionComponent<IComponent> = ({ model }) => {
   const [passportValue, setPassportValue] = useState(model.passport);
   const [addressValue, setAddressValue] = useState(model.address);

  const changePassport = (e: React.ChangeEvent<HTMLInputElement>) => {
      model.passport = e.target.value;
      setPassportValue(e.target.value);
  }

  const changeAddress = (e: React.ChangeEvent<HTMLInputElement>) => {
      model.address = e.target.value;
      setAddressValue(e.target.value);
  }

  return (
  <React.Fragment>
   <div>
    <label htmlFor="passport">Passport:</label>
    <input name="passport" value={passportValue} onChange={changePassport} />
   </div>
   <div>
    <label htmlFor="address">Address:</label>
    <input name="address" value={addressValue} onChange={changeAddress}/>
   </div>
  </React.Fragment>);
};

我不会为此推荐 redux。 Redux 解决了让数据对整个应用程序可用的问题,但我们的表单是 self-contained 所以它不是完成这项工作的正确工具。如果表单开始变得非常复杂,您可能会考虑对表单使用 Context,但现在没有必要。

您想要的是将表单状态存储在表单的 top-level 并传递给特定组件。您不希望各个组件管理自己的状态。

我看不到对表单数据使用 class 的好的用例。您希望将数据存储在 interface 中。如果以后需要class,可以从data接口构造

export interface Model {
  firstName: string;
  lastName: string;
  passport: string;
  address: string;
}

我们使用 useState 代替使用 useRef 来存储 class 实例,它允许同时存储和更新表单数据。我的第一直觉是状态类型需要是 Partial<Model> 因为我们开始时所有字段都是空的。但由于这些都是 string 字段,我们可以创建一个 initialModel,每个 属性 都有空字符串,并且是一个完整的 Model.

const initialModel: Model = {
  firstName: "",
  lastName: "",
  passport: "",
  address: ""
};

const [model, setModel] = useState<Model>(initialModel);

Component1Component2 需要获取当前的 model 作为道具,他们还需要获取更新模型的方法。我们可以将 setModel 作为 prop 传递,这很好,但我们最终会重复逻辑,因为每次调用 setModel 更新单个 属性.

相反,我们可以创建并传递一个更新单个 属性 的辅助函数。此函数采用 property 的名称和我们将其设置为的新 value

const setProperty = (property, value) => {
  setModel({
    ...model,
    [property]: value
  });
};

我不知道你的打字稿知识有多高深。如果 Model 具有具有不同值类型的不同字段,我们希望使用 generic 来确保值的类型与 属性.

相匹配
export type SetPropertyFunction = <T extends keyof Model>(
  property: T,
  value: Model[T]
) => void;

但由于我们所有的值都在这里 string,我们可以通过更简单的方法来解决问题。

export type SetPropertyFunction = (
  property: keyof Model,
  value: string
) => void;

我们的 sub-components 将收到的道具是 modelsetProperty 回调。

export interface ComponentProps {
  model: Model;
  setProperty: SetPropertyFunction;
}

export type FormComponent = React.FunctionComponent<ComponentProps>;

完成应用组件

export const App: React.FunctionComponent<{}> = () => {
  const [model, setModel] = useState<Model>(initialModel);

  const setProperty: SetPropertyFunction = (property, value) => {
    setModel({
      ...model,
      [property]: value
    });
  };

  const save = () => {
    console.log(model);
  };

  return (
    <React.Fragment>
      <Component1 model={model} setProperty={setProperty} />
      <Component2 model={model} setProperty={setProperty} />
      <button onClick={save}>Сохранить</button>
    </React.Fragment>
  );
};

现在 Component1Component2。他们不再管理任何内部状态。
他们可以从模型中获取每个字段的 valuemodel.firstName。在 onChange 处理程序中,我们使用 property.

的名称调用我们的 setProperty 助手
const changeFirstName = (e: React.ChangeEvent<HTMLInputElement>) => {
  setProperty("firstName", e.target.value);
};

const changeLastName = (e: React.ChangeEvent<HTMLInputElement>) => {
  setProperty("lastName", e.target.value);
};

这些回调非常简单,我们也可以内联编写它们。

onChange={e => setProperty("firstName", e.target.value)}

我们的组件现在只处理演示。这是separation of concerns,不错!

完成组件 1

export const Component1: FormComponent = ({ model, setProperty }) => {
  return (
    <React.Fragment>
      <div>
        <label htmlFor="firstName">FirstName:</label>
        <input
          name="firstName"
          value={model.firstName}
          onChange={e => setProperty("firstName", e.target.value)}
        />
      </div>
      <div>
        <label htmlFor="lastName">LastName:</label>
        <input
          name="lastName"
          value={model.lastName}
          onChange={e => setProperty("lastName", e.target.value)}
        />
      </div>
    </React.Fragment>
  );
};

StackBlitz Link