我正在使用 Redux。我应该在 Redux 存储中管理受控输入状态还是在组件级别使用 setState?

I am using Redux. Should I manage controlled input state in the Redux store or use setState at the component level?

我一直在努力找出管理我的 React 表单的最佳方式。我尝试使用 onChange 触发一个动作并用我的表单数据更新我的 redux 存储。我还尝试创建本地状态,当我提交表单时,我触发并操作并更新 redux 存储。

我应该如何管理我的受控输入状态?

  1. 您可以使用组件自身的状态。然后采用该状态并将其作为操作的参数。这几乎就是 React Docs.

    中描述的“React 方式”
  2. 您也可以查看Redux Form。它基本上完成您所描述的并将表单输入与 Redux 状态链接起来。

第一种方式基本上意味着您手动执行所有操作 - 最大控制和最大样板。第二种方式意味着您让高阶组件为您完成所有工作。然后是介于两者之间的一切。我看到有多个软件包可以简化表单管理的特定方面:

  1. React Forms - 它提供了一堆帮助组件,使表单渲染和验证更加简单。

  2. React JSON schema - 允许从 JSON 模式构建 HTML 表单。

  3. Formsy React - 正如描述所说:“React JS 的这个扩展旨在成为灵活性和可重用性之间的“最佳点”。”

更新: 似乎最近 Redux Form 被替换为:

  1. React Final Form

space 中另一个值得一试的重要竞争者是:

  1. Formik

在我的上一个项目中尝试了 React Hook Form - 非常简单,占地面积小并且可以正常工作:

  1. React Hook Form

就我个人而言,我强烈建议将所有内容都保持在 Redux 状态并远离本地组件状态。这本质上是因为如果您开始将 ui 视为状态的函数,您可以进行完整的无浏览器测试,并且可以利用保留完整状态历史记录的参考(例如,他们的输入中有什么,什么对话框是开放的,等等,当一个错误命中时 - 不是他们从一开始的状态)用于调试目的的用户。 Related tweet from the realm of clojure

编辑补充:这是我们和我们的姊妹公司在我们的生产应用程序和我们处理方式方面的进展redux/state/ui

我喜欢 Redux 合著者之一的回答: https://github.com/reactjs/redux/issues/1287

Use React for ephemeral state that doesn't matter to the app globally and doesn't mutate in complex ways. For example, a toggle in some UI element, a form input state. Use Redux for state that matters globally or is mutated in complex ways. For example, cached users, or a post draft.

Sometimes you'll want to move from Redux state to React state (when storing something in Redux gets awkward) or the other way around (when more components need to have access to some state that used to be local).

The rule of thumb is: do whatever is less awkward.

也就是说,如果您确定您的表单不会影响全局状态或需要在您的组件卸载后保留,那么请保持反应状态。

TL;DR

可以使用适合您的应用的任何内容(来源:Redux docs


Some common rules of thumb for determing what kind of data should be put into Redux:

  • Do other parts of the application care about this data?
  • Do you need to be able to create further derived data based on this original data?
  • Is the same data being used to drive multiple components?
  • Is there value to you in being able to restore this state to a given point in time (ie, time travel debugging)?
  • Do you want to cache the data (ie, use what's in state if it's already there instead of re-requesting it)?

这些问题可以轻松帮助您确定更适合您的应用的方法。以下是我在我的应用程序(表单)中使用的观点和方法:

本地状态

  • 当我的表单与 UI 的其他组件无关时很有用。只需从 input(s) 捕获数据并提交。我大部分时间都使用它来制作简单的表格。
  • 我在调试表单输入流的时间旅行中没有看到太多用例(除非其他 UI 组件依赖于此)。

Redux 状态

  • 当表单必须更新我的应用程序中的其他一些 UI 组件时很有用(很像 two-way binding)。
  • 当我的表单 input(s) 根据用户输入的内容导致其他一些组件 render 时,我会使用它。

使用帮助程序库更快,并且避免了我们所有的样板文件。它们可能经过优化、功能丰富……等等。因为它们使所有不同的方面都变得轻而易举。测试它们并使您的武器库知道什么对不同的需求有用和更好,这就是要做的事情。

但是如果您已经自己实现了所有内容。走可控的道路。出于某种原因 你需要 redux。在我的一个项目中。我需要维护表单状态。因此,如果我转到另一个页面并返回,它将保持相同的状态。你只需要 redux,如果它是将更改传达给多个组件的一种方式。或者,如果它是存储状态的一种方式,则您需要恢复。

如果状态需要是全局的,你需要 redux。否则你不需要它。就此而言,您可以查看这篇很棒的文章 here 进行深入研究。

你可能会遇到的问题之一!使用受控输入时。您是否可以在每次击键时发送更改。您的表格将开始冻结。它变成了一只蜗牛。

永远不要在每次更改时直接调度和使用 redux flux。 您可以做的是将输入状态存储在组件本地状态中。并使用 setState() 进行更新。一旦状态改变,你就可以设置一个有延迟的定时器。每次击键都会取消它。最后一次击键将在指定延迟后执行调度操作。(最好的延迟可能是 500 毫秒)。

(知道 setState,默认情况下有效地处理多次连续击键。否则我们会使用与上面提到的相同的技术(就像我们在 vanilla js 中所做的那样)[但这里我们将依赖 setState])

下面是一个例子:

onInputsChange(change, evt) {
    const { onInputsChange, roomId } = this.props;

    this.setState({
        ...this.state,
        inputs: {
            ...this.state.inputs,
            ...change
        }
    }, () => {
        // here how you implement the delay timer
        clearTimeout(this.onInputsChangeTimeoutHandler); // we clear at ever keystroke
              // this handler is declared in the constructor
        this.onInputsChangeTimeoutHandler = setTimeout(() => {
           // this will be executed only after the last keystroke (following the delay)
            if (typeof onInputsChange === "function")
                    onInputsChange(this.state.inputs, roomId, evt);
        }, 500);
    })
}

您可以使用反模式来使用 props 初始化组件,如下所示:

constructor(props) {
    super(props);

    const {
        name,
        description
    } = this.props;

    this.state = {
        inputs: {
            name,
            description
        }
    }

在构造函数中或在 componentDidMount 钩子中,如下所示:

componentDidMount () {
    const {
        name, 
        description
    } = this.props;

    this.setState({
        ...this.state,
        inputs: {
            name,
            description
        }
    });
}

后者允许我们在每个组件安装时从存储中恢复状态。

此外,如果您需要从父组件更改表单,您可以向该父组件公开一个函数。通过为 setInputs() 方法设置 绑定 。在构造中,你执行道具(即getter方法)getSetInputs()。 (一个有用的例子是当你想在某些条件或状态下重置表格时)。

constructor(props) {
    super(props);
    const {
         getSetInputs
    } = this.props;

   // .....
   if (typeof getSetInputs === 'function') getSetInputs(this.setInputs);
}

为了更好地理解我在上面所做的事情,这里是我更新输入的方式:

// inputs change handlers
onNameChange(evt) {
    const { value } = evt.target;

    this.onInputsChange(
        {
            name: value
        },
        evt
    );
}

onDescriptionChange(evt) {
    const { value } = evt.target;

    this.onInputsChange(
        {
            description: value
        },
        evt
    );
}

/**
 * change = {
 *      name: value
 * }
 */
onInputsChange(change, evt) {
    const { onInputsChange, roomId } = this.props;

    this.setState({
        ...this.state,
        inputs: {
            ...this.state.inputs,
            ...change
        }
    }, () => {
        clearTimeout(this.onInputsChangeTimeoutHandler);
        this.onInputsChangeTimeoutHandler = setTimeout(() => {
            if (typeof onInputsChange === "function")
                onInputsChange(change, roomId, evt);
        }, 500);
    })
}

这是我的表格:

 const {
        name='',
        description=''
 } = this.state.inputs;

// ....

<Form className="form">
    <Row form>
        <Col md={6}>
            <FormGroup>
                <Label>{t("Name")}</Label>
                <Input
                    type="text"
                    value={name}
                    disabled={state === "view"}
                    onChange={this.onNameChange}
                />
                {state !== "view" && (
                    <Fragment>
                        <FormFeedback
                            invalid={
                                errors.name
                                    ? "true"
                                    : "false"
                            }
                        >
                            {errors.name !== true
                                ? errors.name
                                : t(
                                        "You need to enter a no existing name"
                                    )}
                        </FormFeedback>
                        <FormText>
                            {t(
                                "Enter a unique name"
                            )}
                        </FormText>
                    </Fragment>
                )}
            </FormGroup>
        </Col>
        {/* <Col md={6}>
            <div className="image">Image go here (one day)</div>
        </Col> */}
    </Row>

    <FormGroup>
        <Label>{t("Description")}</Label>
        <Input
            type="textarea"
            value={description}
            disabled={state === "view"}
            onChange={this.onDescriptionChange}
        />
        {state !== "view" && (
            <FormFeedback
                invalid={
                    errors.description
                        ? "true"
                        : "false"
                }
            >
                {errors.description}
            </FormFeedback>
        )}
    </FormGroup>
</Form>