React 最佳实践:动态表单

6,398 阅读3分钟

具体场景

基于 Ant Design, 我们来实现一个【薪资调查表】,需求有二:

  • 职位(Job)列表 模拟从服务端拉取 form-2.png

  • 当职位是 Student 时,薪资(Income) 输入框消失。 form-3.png

    form-4.png

实现

UI 描述

form-1.png

我们先用 Ant Design 提供的 Form, Input, Select, Button 组件来构建 UI 架构。

  • labelCollabel 标签布局,wrapperCol为输入控件设置布局样式,使用方式同 Grid 栅格

  • 使用 <Button htmlType="submit" /> 调用 web 原生提交逻辑。

import { Form, Input, Select, Button } from 'antd';
const { Option } = Select;

const layout = {
  labelCol: { span: 6 },
  wrapperCol: { span: 12 },
};

const DynamicForm = () => {
  return (
    <Form {...layout}>
      <Form.Item name="name" label="User Name">
        <Input />
      </Form.Item>
      <Form.Item name="job" label="Job">
        <Select placeholder="Select a option and change input text above">
          <Option>loading</Option>
        </Select>
      </Form.Item>
      <Form.Item name="income" label="Income">
        <Input />
      </Form.Item>
      <Form.Item>
        <Button type="primary" htmlType="submit">
          Submit
        </Button>
      </Form.Item>
    </Form>
  );
};

export default DynamicForm;

基础 UI 框架完成后,我们来实现开头的两个逻辑需求。

模拟服务端拉取列表

先介绍一下 useState 和 useEffect 两个 Hooks 吧:

useState

const [state, setState] = useState(initialState);

它会返回一个 state,以及更新 state 的函数。

initialState 会作为 state 的初始值。

useEffect

useEffect(didUpdate);

该 Hook 接收一个包含命令式、且可能有副作用代码的函数。

赋值给 useEffect 的函数会在组件渲染到屏幕之后执行,改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作,可以放在 useEffect 里执行。

组件卸载时需要清除 effect 创建的诸如订阅或计时器 ID 等资源。要实现这一点,useEffect 函数需返回一个清除函数,我们在下面也会用到。

默认情况下,effect 会在每轮组件渲染完成后执行。然而,我们只想在某些特定条件下执行 effect,该怎么办呢?

要实现这一点,可以给 useEffect 传递第二个参数,只有当 props.source 改变后才会重新创建订阅。

useEffect(didiUpdate, [props.source]);

实现

import { useState, useEffect } from 'react';

const DynamicForm = () => {
  const [jobs, setJobs] = useState([]);

  useEffect(() => {
    const timer = setTimeout(() => {
      setJobs(['engineer', 'student', 'doctor']);
    }, 2000);
    return function clear() {
      clearTimeout(timer);
    };
  }, []); // [],只执行一次

  return;
};

在列表数据拉取完成之后,遍历 jobs 渲染下拉项:

<Select>
  {jobs.length > 0 ? (
    jobs.map((job) => (
      <Option key={job} value={job}>
        {job}
      </Option>
    ))
  ) : (
    <Option>loading</Option>
  )}
</Select>

动态表单元素

Job 字段值为 Student 时,Income 输入框需要隐藏。这样的话,我们需要能获取 Job 的字段值。

在函数组件中,通过 Form.useForm 对表单数据域进行交互。getFieldValue 方法可以获取对应字段名的值。

const DynamicForm = () => {
  const [form] = Form.useForm();
  return (
    <Form form={form} >
      {({ getFieldValue }) =>
          getFieldValue('job') !== 'student' ? (
            <Form.Item
              name="income"
              label="Income"
            >
              <Input />
            </Form.Item>
          ) : null
        }
    </Form>
};

我们希望在修改 Job 字段值后更新 Income 输入控件的显隐,shouldUpdate 可以帮助我们实现这个更新逻辑。

<Form.Item
  shouldUpdate={(prevValues, currentValues) =>
    prevValues.job !== currentValues.job
  }
></Form.Item>

但这又引发了另一个问题,<Form.Item name="field" /> 只会对它的直接子元素绑定表单功能,就像这样:

<Form.Item label="Field" name="field">
  <Input />
</Form.Item>

所以我们需要通过添加 noStyle 将外层 Form.Item 作为纯粹的无样式绑定组件。

实现

<Form.Item
  noStyle
  shouldUpdate={(prevValues, currentValues) =>
    prevValues.job !== currentValues.job
  }
>
  {({ getFieldValue }) =>
    getFieldValue('job') !== 'student' ? (
      <Form.Item name="income" label="Income" rules={[{ required: true }]}>
        <Input />
      </Form.Item>
    ) : null
  }
</Form.Item>

动态表单完整代码

import { useState, useEffect } from 'react';
import { Form, Input, Select, Button } from 'antd';

const { Option } = Select;

const DynamicForm = () => {
  const [form] = Form.useForm();
  const [jobs, setJobs] = useState([]);

  const layout = {
    labelCol: { span: 6 },
    wrapperCol: { span: 12 },
  };

  useEffect(() => {
    const timer = setTimeout(() => {
      setJobs(['engineer', 'student', 'doctor']);
    }, 2000);
    return function clear() {
      clearTimeout(timer);
    };
  }, []);

  return (
    <Form form={form} {...layout}>
      <Form.Item name="name" label="User Name" rules={[{ required: true }]}>
        <Input />
      </Form.Item>
      <Form.Item name="job" label="Job" rules={[{ required: true }]}>
        <Select
          placeholder="Select a option and change input text above"
          allowClear
        >
          {jobs.length > 0 ? (
            jobs.map((job) => (
              <Option key={job} value={job}>
                {job}
              </Option>
            ))
          ) : (
            <Option>loading</Option>
          )}
        </Select>
      </Form.Item>
      <Form.Item
        noStyle
        shouldUpdate={(prevValues, currentValues) =>
          prevValues.job !== currentValues.job
        }
      >
        {({ getFieldValue }) =>
          getFieldValue('job') !== 'student' ? (
            <Form.Item
              name="income"
              label="Income"
              rules={[{ required: true }]}
            >
              <Input />
            </Form.Item>
          ) : null
        }
      </Form.Item>
      <Form.Item>
        <Button type="primary" htmlType="submit">
          Submit
        </Button>
      </Form.Item>
    </Form>
  );
};

export default DynamicForm;

React 最佳实践