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

Posted

技术标签:

【中文标题】我正在使用 Redux。我应该在 Redux 存储中管理受控输入状态还是在组件级别使用 setState?【英文标题】:I am using Redux. Should I manage controlled input state in the Redux store or use setState at the component level? 【发布时间】:2016-04-29 09:31:51 【问题描述】:

我一直在尝试找出管理我的反应表单的最佳方法。我尝试使用 onChange 来触发操作并使用我的表单数据更新我的 redux 存储。我也尝试过创建本地状态,当我的表单被提交时,我触发并操作并更新 redux 存储。

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

【问题讨论】:

@Dylan 不幸的是,URL 现在抛出 404。 github.com/reactjs/redux/issues/159 【参考方案1】:

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

将 React 用于与全局应用无关的短暂状态 并且不会以复杂的方式变异。例如,某些 UI 中的切换 元素,表单输入状态。将 Redux 用于全局重要的状态 或以复杂的方式发生突变。例如,缓存的用户或帖子 草稿。

有时你会想要从 Redux 状态转移到 React 状态(当 在 Redux 中存储一些东西会变得很尴尬)或相反(当 更多的组件需要访问一些过去的状态 本地)。

经验法则是:做不那么尴尬的事情。

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

【讨论】:

【参考方案2】:

    您可以使用组件自己的状态。然后获取该状态并将其作为操作的参数。这几乎就是React Docs 中描述的“反应方式”。

    您也可以查看Redux Form。它基本上完成了您所描述的操作,并将表单输入与 Redux State 链接起来。

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

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

    React JSON schema - 允许从 JSON 模式构建 html 表单。

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

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

    React Final Form

该领域另一个值得一试的重要竞争者是:

    Formik

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

    React Hook Form

【讨论】:

我认为这取决于情况。如果其他组件也需要该状态,那么您可能希望将其保存在 redux 中。 另一个因素是在每次击键时调度事件的潜在开销,并导致所有其他组件重新渲染。当然,这可以通过使用shouldComponentUpdate 进行优化,但您必须注意一些事情。最终,没有一个绝对的答案 - 找到适合您的应用程序的东西,然后继续使用它。值得一提的是,我最近制作了一个包装器组件的原型,该组件处理其状态的当前更新,并触发去抖动操作以更新商店 (visible in this gist) 我继续尝试了 Redux Form。效果很棒。它使用各种事件 onChange、onBlur、onFocus。它调度了很多动作,但似乎非常有效。我们可以从这个问题中得出一个明确的答案,还是这更像是一个有争议/情境的事情? @gkkirsch 与 Redux 中的所有内容一样,这是您的设计决定。像 Angular 这样的框架要求你以某种方式做事。在这里,一切都是关于管理权衡。顺便说一句,更新了答案,提供了更多可供考虑的选项。 @Casimir 当您的应用程序变得更复杂时,问题就开始了 - 比如说您向表单添加多语言支持,添加一些自定义组件,例如颜色选择器,添加一个提示以防止用户在保存之前刷新页面等等。我现在不记得具体的问题了,但总的来说 - 太神奇了。而且看起来这个项目现在不是很活跃,所以我认为现在使用它不是一个好的长期决定。如果您喜欢它,切换到 Final Form 可能更有意义 - 它由同一开发人员制作,并且具有相似的 API。【参考方案3】:

TL;DR

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


确定数据类型的一些常用经验法则 放入 Redux:

应用程序的其他部分是否关心这些数据? 您是否需要能够基于此原始数据创建进一步的派生数据? 是否使用相同的数据来驱动多个组件? 能够将此状态恢复到给定的时间点(即时间旅行调试)对您有价值吗? 是否要缓存数据(即,如果它已经存在,则使用状态中的内容,而不是重新请求它)?

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

本地状态

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

Redux 状态

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

【讨论】:

【参考方案4】:

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

编辑添加:这是我们和我们的姊妹公司在生产应用程序以及我们如何处理 redux/state/ui 方面的发展方向

【讨论】:

【参考方案5】:

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

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

如果 state 需要是全局的,你需要 redux。否则你不需要它。就此而言,您可以查看这篇精彩的文章 here 进行深入了解。

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

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

(知道setState,默认情况下有效地处理多个连续击键。否则我们将使用与上述相同的技术(就像我们在香草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()方法。在构造过程中,您执行 props(即 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>

【讨论】:

以上是关于我正在使用 Redux。我应该在 Redux 存储中管理受控输入状态还是在组件级别使用 setState?的主要内容,如果未能解决你的问题,请参考以下文章

React + Redux + Router - 我应该为所有页面/组件使用一个状态/存储吗?

我应该将函数引用存储在 Redux 存储中吗?

我应该直接使用 Apollo 数据还是在 Redux 存储中保存/修改它?

React Native 已经有了异步存储。为啥我应该在我的 react native 应用中使用 Redux 和 Redux Thunk?

如何在 Redux/React 中处理 UI 状态 - Redux-UI?

如何修复 React/Redux/Firebase 错误上传文件到存储