让我们使用此线程来讨论 componentWillMount 的用例以及这些问题的替代解决方案。 通常,解决方案是在必要时使用 componentDidMount 和两遍渲染。
在“componentWill”阶段做全局副作用有几个问题。 这包括启动网络请求或订阅 Flux 商店等。
1) 与错误边界一起使用时会造成混淆,因为当前componentWillUnmount
可以在不调用componentDidMount
的情况下调用。 componentWill*
是一个虚假的承诺,直到所有孩子都成功完成。 目前,这仅适用于使用错误边界的情况,但我们可能希望恢复此决定,而不是在此处调用componentWillUnmount
。
2)当一个新的渲染因为更高优先级的更新中断它而中止时,Fiber 实验并没有真正有一个很好的方法来调用componentWillUnmount
。 类似地,我们的姊妹项目ComponentKit在线程中进行协调,在这些线程中执行副作用尚不安全。
3)从回调componentWillMount
与更新父组件的setState
是完全不支持的,并导致奇怪和顺序相关的竞态条件。 我们已经知道我们要弃用这种模式。
4) 如果您在componentWillMount
执行全局副作用,则可以很容易地依赖于孩子的和解顺序。 它们还没有得到完全保证,因为更新可能会导致意外的对帐订单。 依赖顺序还限制了未来的用例,例如异步或流式渲染和并行化渲染。
componentWillMount
的唯一合法用例是自己调用this.setState
。 即便如此,您也永远不需要它,因为您可以将初始状态初始化为您拥有的任何内容。 我们只在一个非常具体的用例中保留它:
class Foo {
state = { data: null };
// ANTI-PATTERN
componentWillMount() {
this._subscription = GlobalStore.getFromCacheOrFetch(data => this.setState({ data: data });
}
componentWillUnmount() {
if (this._subscription) {
GlobalStore.cancel(this._subscription);
}
}
...
}
当同一个回调可以同步和异步使用时,如果数据已经可用,可以方便地避免额外的重新渲染。
解决方案是将这个 API 拆分为同步版本和异步版本。
class Foo {
state = { data: GlobalStore.getFromCacheOrNull() };
componentDidMount() {
if (!this.state.data) {
this._subscription = GlobalStore.fetch(data => this.setState({ data: data });
}
}
componentWillUnmount() {
if (this._subscription) {
GlobalStore.cancel(this._subscription);
}
}
...
}
这保证只有在组件成功挂载时才会发生副作用。 如果需要异步副作用,则无论如何都需要两遍渲染。
我认为这不是太多样板,因为无论如何您都需要componentWillUnmount
。 这都可以隐藏在高阶组件中。
componentWillReceiveProps
和componentWillUpdate
中的全局副作用也很糟糕,因为它们不能保证完成。 由于中止或错误。 如果可能,您应该更喜欢componentDidUpdate
。 但是,即使它们的用例受到限制,它们也可能会保持某种形式。 它们也没有那么糟糕,因为它们仍然会调用componentWillUnmount
进行清理。
我主要使用 cWM 来根据初始道具执行 setState,因为 cWRP 最初不会触发。 我可以在构造函数中做到这一点,但我尽量避免扩展该方法,因为我对超级调用过敏:/
@yaycmyk如果您可以使用字段初始值设定项,您会使用它吗?
class Foo {
state = { data: this.props.initialData };
...
}
或者更复杂的东西:
class Foo {
state = this.computeInitialState();
computeInitialState() {
var state = { data: null};
if (this.props.something) {
state.data = this.props.somethingElse;
}
return state;
}
...
}
for createClass cWM 是唯一可以做构造函数的地方,因为您实际上无法定义构造函数。 我需要通过我们的一些代码库做一个 grep 我认为还有其他几个用例/优化使用它我不记得了
@sebmarkbage我确实使用了属性初始化器,但如果您需要大量的布尔逻辑,它们会变得笨拙。 公平地说,它们确实涵盖了我所有用例的 95%。 cWM 在我们的产品中很少见
对于服务器渲染,componentWillMount 会触发,但 componentDidMount 不会触发。
什么会取代 componentDidMount 的仅浏览器性质? 与当前的生命周期函数方法相比,在 componentDidMount 中添加if (typeof window === 'undefined')
表达式来检测渲染模式似乎很脏。
@mars componentDidMount
仍然只会在客户端上触发。 您是否考虑过服务器上componentWillMount
的用例?
我只用它来在this
上设置数据结构
回顾我上一个使用服务器渲染的应用程序后,唯一的 componentWillMount 用例是初始setState
。 因此,按照@sebmarkbage的建议在 componentDidMount 中有条件地setState
将是允许弃用 componentWillMount 的好方法。
这个问题可能是因为我最近的一些黑客行为。 今天,考虑到当componentWillMount
被调用时,你实际上可以相信它会被挂载,你可以做一些事情,比如将后代注册到一个祖先,并在另一个祖先的渲染中使用该信息。
http://codepen.io/anon/pen/gwbEJy?editors=0010
作为一种 DOM 并行,当您的表单有两个输入,其中一个是按钮时,文本输入的“keyDown”会导致表单提交。 换句话说,另一个祖先元素的存在或不存在会改变另一个祖先元素的行为。 还有广播组。
不是说“保留 componentWillMount”,但这是在上下文的祖先之间创建关系的好方法。 1)有一个componentMountCancelled(听起来不太可能,或2)在服务器上调用cDM(???)因为这是我在cWM或3中这样做的唯一原因,或者3)有一些一流的API来构建这种类型会很好同一语境下祖先之间的关系。
我想要 (3),但不知道那是什么样子。
对于此处的用例,它位于 React Router v4 API 中:
<div>
<Match pattern="/users" component={Users}/>
<Match pattern="/about" component={About}/>
<Miss component={NoMatch}/>
</div>
如果没有<Match/>
匹配位置,那么上下文中的后代<Miss/>
组件将被渲染(它很漂亮,不是吗?!)
这很重要的唯一原因是服务器渲染。 感谢与@sebmarkbage 的一些谈话,我们有一个计划 B,但我对此有点😢。 (我认为当我们转向 cDM 时,我们可能会看到一个闪烁......这并不令人兴奋。)
我很确定@kenwheeler的新<Song>
采用了这种技术,但可能会转移到cDM
。
@ryanflorence为什么不只是在这种情况下扩展构造函数呢? cDM 确实是最后的手段,因为它需要第二次渲染通道的效率较低。
是否会出现我们需要在组件安装之前计算一些数据或执行某个例程,否则我们将无法在组件构造函数中执行的情况?
如果我们需要或想要在组件第一次挂载之前 _before_ 触发 Redux 操作怎么办? 或者想要在安装之前将事件侦听器附加到组件怎么办?
我只是担心componentWillMount
可能会有一些我们没有想到的利基用例。
@ajwhite我很好奇你对此有何看法
如果我们需要或想要在组件第一次挂载之前触发 Redux 操作怎么办?
@nickzuber我只是来提这个的。 我在使用react-rails为服务器渲染较小的 react/redux 客户端应用程序集合的componentWillMount
初始数据填充我们的减速器。 这样你就可以使用react_component轻松地将数据从 rails 视图层传递到你的应用程序中,并在渲染之前填充状态。
@ryanflorence这更多是为了响应 Fiber 并实现componentWillUnmount
。
对于您的用例,您实际上并不需要componentWillUnmount
因为该用例无论如何都在服务器上。 你也不需要setState
。 因此,从技术上讲,即使不赞成,您仍然可以在构造函数中执行相同的操作。 :) 但是,我认为对于您的用例,仅服务器端componentDidMount
可能最有意义( componentDidServerRender
?)。 它会在渲染后触发,因此它仍然需要两次通过,但保留了其他功能。
@aflanagan你能使用我上面使用的字段初始化器模式(或构造器)从道具初始化状态吗?
@sebmarkbage啊,是的,我想我们可以做到。 在迁移到 es6 类之前,我们采用了cWM
策略,我没有想到我们可以在构造函数中开始这样做。
为了帮助解决服务器用例,像 componentDidRender 这样的新方法怎么样? 对于浏览器生命周期,它会出现在 cDM 之前。 JSX 的想法已被渲染/“编译”到 vdom 中,但尚未安装在某处。
有一个用例,它也与我所知道的componentWillUpdate
的唯一用例相同。 基本上,您读取了一些全局可变状态,以便您可以稍后在componentDidUpdate
重置它,在孩子已经呈现之后。
例如,如果子项渲染导致页面的滚动位置发生变化。
class ScrollRestore {
componentWillMount() {
this._scroll = Global.scrollTop;
}
componentDidMount() {
Global.scrollTop = this._scroll;
}
componentWillUpdate() {
this._scroll = Global.scrollTop;
}
componentDidUpdate() {
Global.scrollTop = this._scroll;
}
...
}
从技术上讲,你可以在构造函数中做同样的事情,但是有不纯的构造函数有点不确定。
只是想说明一下componentWillMount
在服务器渲染和侧加载用例中会引起很多混乱,在这些用例中,它似乎是一个不错的选择,但由于它是同步的,因此并没有真正按照您预期的方式工作。 我认为必须重新考虑这种弃用的唯一用例是当您仍在使用React.createClass
并且没有方便的方法添加到构造函数时。
Airbnb 对componentWillMount
的主要用例是为我们的全局翻译单例提供短语,这些短语来自 props,在渲染路径中需要,并且在构建时不一定可用; 并使用道具引导 Alt 商店。
如果不推荐使用此生命周期方法,我们所有的翻译都将在前一种情况下中断。 由于我们主要在树的顶层处理短语,因此排序不确定性不是我们关心的问题。
此外,构造函数不应该有副作用—— componentWillMount
为我们提供了一个合适的地方来放置副作用,基本上每个环境应该只发生一次,在客户端和服务器上都执行( componentDidMount
,当然,将仅用于客户端)。
认为此 GitHub 搜索可能有助于查看一些用例,按最近的索引排序,因此它代表了人们今天在野外积极使用 componentWillMount 的情况。
注意:您必须登录才能在所有公共存储库中搜索代码
@ljharb所以,它基本上是全局状态的延迟初始化。 我们在render
方法中做了很多惰性初始化,但由于它通常不被观察到,我不认为这是不好的做法。 这里的区别在于它不仅是懒惰的,而且还依赖于 props。
我猜你假设 props 永远不会改变,并且你永远不会有两个不同的子树和不同的短语。 否则,人们通常在此用例中使用context
。
鉴于此特定用例中已经涉及很多假设,也许在构造函数中将其作为 hack 进行并没有那么糟糕?
你如何做这个服务器端? 您是否假设上下文一次仅用于单个请求? 没有异步渲染?
除了构造函数之外,我非常喜欢其他东西。 嗯,我以为context
已经被弃用了 :-p 不是吗?
是的,没错 - 无论它是否是异步的,它都会在每个请求的新vm
重新评估和重建。
如果componentWillUpdate
或componentWillReceiveProps
或类似的东西,可以确保在渲染之前运行每组新的道具,包括第一个/初始的道具 - 这足以让我们使用我认为,并且可能比componentWillMount
更有效地解决它们。 添加新的生命周期方法是不可能的吗?
在许多情况下,添加一致的生命周期方法来处理 props 会非常有用。 最常见的是“为 this.props.xId 获取 x”。 在渲染之前运行的好处是您可以显示加载指示器而无需第二次渲染。 问题是,对于某些用例,您希望它在服务器上运行,例如@ljharb的,但对于获取数据,在大多数情况下您只希望在客户端上运行。
编辑:也许这应该分支到另一个问题。
是否曾经(在浏览器上下文中)在构造函数之后不立即调用 componentWillMount?
据我所知,componentWillMount 实际上是一个构造函数。 在采用带有自己的构造函数的 ES6 类之前,它服务于这个目的。 事实上,在更早的 React 版本中,componentWillMount 在构造 IIRC 时_总是_被调用,这使得服务器渲染变得痛苦。 因此我们将 componentWillMount 更改为仅在浏览器渲染上下文中调用,而不是在服务器渲染上下文中调用。 (然后 getInitialState 是有效的构造函数)
我见过的 componentWillMount 的所有用途都只是您希望仅在浏览器中运行的构造函数。 如果 componentDidMount 不是移动该代码的正确位置(由于双重渲染),则使用typeof window
将其移动到类构造函数中,如果检查可能足够。
如果您不担心在组件构造的一部分 componentWillMount 中做同样的事情,我认为没有理由担心在 React 组件构造函数中产生副作用。 由于用户代码从不直接实例化组件(库代码会),避免副作用构造函数的典型原因不适用于 React 组件。
塞巴斯蒂安,也许另一种选择是稍微改变 componentWillMount 的行为,而不是通过使其更明确地成为构造的一部分来删除它,以避免用户编写浏览器检查的代码并保留大部分现有代码? 这在 Fiber 中是可能的吗?
我不认为我曾经将componentWillMount
用于执行setState(valueDerivedFromProps)
之外的任何其他事情,现在可以在constructor
。
这里唯一的阻碍是constructor
不是React.createClass
配置字段。 就此而言,由于React.createClass
和基于 ES6 类的 API 之间存在差异,我看到了三种可能性:
React.createClass
API(我不知道是否计划在可预见的未来)React.createClass
中引入一个与 ES6 类constructor
行为一致的新字段componentWillMount
只为名称React.createClass
,但改变自己的行为,以匹配constructor
@ryanflorence对于context
参数传递给super
函数,该函数会将其注入this.context
(参见ReactComponent.js ),因此您可以访问this.context
中的constructor
:
class Widget extends React.Component {
constructor(props, context) {
super(props, context)
this.context.register()
}
componentWillUnmount() {
this.context.unregister()
}
render() {
return React.Children.only(this.props.children)
}
}
如果将在服务器渲染中使用替代 do cWM,那么 cWM 弃用似乎没问题。 如前所述,像 componentWillServerRender 这样的东西会很棒。
@leeb
我见过的 componentWillMount 的所有用途都只是您希望仅在浏览器中运行的构造函数。
这对我来说听起来不对。 componentWillMount 与 componentDidMount 不同,_does_ 在服务器上运行。
@sebmarkbage @bloodyowl如果我在构造函数中工作,并且组件从未真正安装,我仍然遇到与 cWM 相同的问题,不是吗? 我没有机会注销。 对我来说的问题是我在渲染之前不知道这个东西是否真的会被渲染。 或者,如果它没有被渲染,我无法知道它被中止了。
如果有多个继承级别,componentWillMount 会在对象完全构建后执行。
我们在 mobile.twitter.com 上的 50 多个组件中使用componentWillMount
来:
如果需要,大多数(如果不是全部)看起来都可以在constructor
或componentDidMount
。
一些说明: getInitialState
(用于 createClass)、 constructor
和componentWillMount
总是同时发生并且具有相同的功能。 唯一的区别是componentWillMount
可以使用this.setState
而其他人必须返回或初始化state
字段。
这意味着,从技术上讲,您仍然可以使用getInitialState
/ constructor
做任何事情。 然而,已经有一个很好的理解和实践,你不要在那些中做副作用。
弃用componentWillMount
的原因是因为对于它的工作原理以及您可以安全地在其中做什么存在很多混淆,通常答案是_nothing_。 这是一个带有 void return 的生命周期,因此您唯一可以做的就是副作用,并且将它们用于诸如订阅和数据获取之类的事情是很常见的,而没有意识到它不会很好地工作的边缘情况。
目的是高度阻止副作用,因为它们具有不可预见的后果,即使您在技术上可以在构造函数中执行它们。
反过来,这将有助于升级到 Fiber 之类的东西,而无需期望大量竞争条件以微妙的方式破坏您的应用程序。
@ryanflorence如果您在服务器上,您将进行功能测试并且仅在constructor
执行该工作,并且在客户端上您将使用componentDidMount
。 但是,明确的仅服务器componentDidServerRender
会更好。
@mstijak我们通常不鼓励深度继承。 这是我们由于像这样的复杂性而做出的设计决定。 即使您确实使用了它,通常的做法是不依赖超类回调子类进行初始化。
@michalwerner仅在组件树呈现后才触发服务器端的唯一替代方案是否可以? 即语义上componentDidMount
工作的。 或者你依赖它被早期调用?
@sebmarkbage在服务器上,唯一的目的是生成 HTML,所以渲染后做的事情总体来说似乎没什么用 :-)
@ljharb的应用案例@ryanflorence是扔掉缓冲的HTML和做一个重定向或渲染基于什么被渲染不同的反应。 这不一定适用于流式传输,但适用于异步或并行化/分布式渲染。
另一个用例是记录已呈现但不必在生成 HTML 之前完成的组件。
只使用它,这样我就可以避免烦人的构造函数(它更冗长,必须调用 super 等)只是为了设置初始状态
@sebmarkbage我们有两个用例 1) 知道有一个未命中,但保留 HTML 并使用 404 状态,2) 丢弃 HTML 并改为重定向。 在任何一种情况下,正如我们私下讨论的那样,我们可以对 (1) 进行两次渲染
我使用componentWillMount
的唯一用例是避免此错误
Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component
就像在依赖于 props 的构造函数中一样,应该在不调用setState
情况下定义状态更改代码,但是可以在其他部分重用该状态更改代码。
顺便说一句,我喜欢删除componentWillMount
的想法,因为它与constructor
相比没有任何优势,但提供了很多关于它有何不同的问题。
@sebmarkbage我认为大多数情况可以通过简单地使用上下文来存储组件是否将安装在服务器context.mountEnv
。 这将使它变得非常简单。 对于 airbnb 的用例,创建一个定义childContextTypes = {mountEnv<string>}
的 HOC 并在构造函数上执行翻译和诸如此类的初始化就足够了。 如果单独的 props 不足以进行此引导,则 Wrapped 组件始终可用于存储静态数据。
我现在想不出单独使用构造函数无法完成的事情。 我可能会开始对下一个版本发出警告,并在稍后弃用它。
我一直在使用componentWillMount
作为依赖库,它将组件包装在 HOC 中以加载客户端和服务器上的数据依赖项。 它的工作原理是使用承诺跟踪所有加载的依赖项并多次调用renderToString
直到不再有新的依赖项要加载。
但是现在想想, componentWillMount
应该不是这个必要的。 您可以简单地使用constructor/getInitialState
调用承诺,然后在加载依赖项后在下一个renderToString
调用中同步设置状态。 客户端仍然可以使用componentDidMount
来设置状态。
我认为弃用componentWillMount
是有道理的,即使从同构应用程序的角度来看也是如此。 我也不认为有必要添加componentDidServerRender
方法,因为您可以轻松地在构造函数中自己编写该逻辑。
@AlexGalays写道:_Only 使用它所以我可以避免烦人的构造函数(它更冗长,必须调用 super 等)只是为了设置初始状态_虽然这可能不是保持componentWillMount
的最重要参数,这是一个重要的。
不能在组件中调用 this.setState() 会挂载吗? 组件将挂载仅用于设置侦听器。 并不是说,你不能在 ComponentDidMount 中做同样的事情,但我认为主要的区别是 setState() 总是在 componentDidMount() 中工作,而在 componentWillMount() 中不起作用。 ComponentWillMount() 和 componentWillUnmount() 已经是设置和拆除侦听器的完美而明确的地方,而不会弄乱 componentDidMount(),而 componentDidMount() 通常还有其他逻辑。 只有我的 2 美分。
@catmando @AlexGalays我认为这更像是一种令人沮丧的使用,然后是软性弃用。 硬性弃用需要很长时间,我预计到那时人们会普遍使用字段初始值设定项。 它们还可以让您避免创建super
构造函数的膨胀。
@jkol36那不正确。 setState()
适用于componentWillMount
以避免第二次渲染。
@sebmarkbage,如果您对此是正确的,那么我会更正。 我确实记得一些实例,尽管当我尝试在 componentWillMount() 中调用 setState() 时会收到错误消息,说“无法在安装组件上调用 setstate”
无论如何要知道在服务器上渲染的组件带有光纤? 那里没有 cDM,我们只有构造函数(或 cWM),我们不能相信它们。
我目前的方法是发送一个对象并对其进行变异,检查对象以查看我们是否需要进行第二遍渲染,然后使用该对象进行第二遍渲染,以了解这次以不同方式渲染的内容。
但据我所知,没有办法知道服务器上渲染或未渲染的内容。
在我的任何项目中都没有一个componentWillMount
,从来不明白为什么需要它。
我只是定义了用于在服务器端获取数据的静态方法,我可以完全控制并确切地知道如何以及为什么调用它们。
通常我不同意componentDidMount 。 React 生命周期就像 getDefaultProps、getInitialState、componentWillMount、render、componentDidMount。 如果您在 componentDidMount 中设置状态,您将重新渲染组件,因此渲染会被调用两次
@sebmarkbage因此,例如,如果我使用 React.createClass 并使用 componentWillMount 获取相同的数据并在内部调用 setState() 那么最好使用 getInitialState 并在 getInitialState 中执行相同操作?
@w-site 查看componentDidMount 文档
那么最好使用 getInitialState 并在 getInitialState 中做同样的事情?
您不能使用getInitialState
来获取数据,因为它是同步的。
用例: componentWillMount
包含组件本地数据请求的调度,这些请求由相应的 redux-saga 异步完成。 如果开启了服务端渲染,sagas会在服务端运行,直到sagas中的所有异步I/O都完成并且不再需要重新渲染,然后将最新的redux状态和最新渲染的HTML发送到再水化的浏览器。
例子:
componentWillMount() {
const { isLoading, isLoaded, dispatch } = this.props;
if (!isLoading && !isLoaded) {
dispatch(makeAction(ACTION_FOO_LOAD_REQUESTED));
}
}
所以只是澄清一下,当前调度 redux 操作以获取数据或订阅的最佳实践是在客户端的 cDM 和服务器端的构造函数中?
@jedwards1211在你渲染任何东西之前获取数据的服务器上,把它放在 redux 存储中,然后 connect 会为你提取它。
@brigand我知道很多人更喜欢在服务器端这样做,但我更喜欢使用一个在安装时订阅并在卸载时取消订阅的组件,并在服务器端使用两遍渲染来获取请求,以便我可以避免必须编写单独的代码来指定在服务器端获取哪些数据。
再说一次,如果我们选择在组件挂载时出于任何原因分派 redux 操作,那么在服务器端执行此操作的最佳位置是在构造函数中?
@jedwards1211有趣。 我会为它编写一个高阶组件。 这是概念。 您可以在 webpack 中使用 DefinePlugin 定义__SERVER__
。
@brigand我的想法完全相同😃 我基本上一直在做同样的事情,除了我使用的是componentWillMount
,所以我只是将该逻辑移到构造函数中。 更详细地说,在客户端,我有一个中间件,它通过进行 websocket RPC 调用来处理订阅操作。 在服务器端,我有中间件,它只将订阅操作存储在与请求关联的数组中。 在第一次渲染后,我会执行这些操作并为它们异步获取初始数据并将其分派到商店。 然后我渲染第二遍并将其发送给客户端。
多通道渲染并不理想,但我认为 React 最终将提供在将虚拟 dom 实际渲染为静态标记之前多次有效更新它的能力,并且保持我的订阅代码 DRY 对我来说非常值得。
我一定是少数认为必须使用构造函数而不是 componentWillMount 是错误的人之一。 这可能只是普通类习语的保留,关于保持构造函数的精简,并且永远不会在其中执行任何不需要初始化对象以执行它可以用于的任何其他事情的操作。
这里有一些关于为什么我不使用构造函数的参数:
构造函数似乎应该是 React 私有的,并且只是一个实现细节。 如果构造函数的签名需要更改怎么办? 当然,我可以盲目地执行 super(...args),但我也看到开发人员一直忽略构造函数的第二个参数,这对我来说似乎是不好的做法。 拥有 componentWillMount 解决了这个问题。
它使子类化组件变得更加困难。 现在,我一般不喜欢继承,尽管我已经在产品中找到了一些适用于它的用法。 如果在这里使用构造函数,那么可能是构造函数在父组件中做的太多,子类中无法擦除。 拥有 componentWillMount 也解决了这个问题,因为我可以选择执行超类的版本。 (虽然这可能不是最好的编程实践,但这不是这里讨论的内容。)
它本质上使组件的单元测试变得更加困难,因为如果我不想调用 componentWillMount 事件,我就不能再将它存根。 在使用酶测试某些其他安装操作时,这已被证明是非常宝贵的,而不必担心实际拨打电话。 而且由于这通常是使用 redux 来提供 props 来完成的,因此通常可以毫无问题地完成。
如果有的话,我认为应该避免使用构造函数,而不是 componentWillMount。
@jamespedid你是说你有时想创建一个组件的实例而不在你的单元测试中安装它? 所以你可以在它上面调用一些方法并测试它们是如何工作的?
@jedwards1211这将是理想的,尽管我不确定它在
@jamespedid所以我不明白你所说的投诉是什么意思。 3 然后。 但我想可能有一种方法可以重组您的代码,从而可以轻松进行单元测试,而无需使用 noop 对componentWillMount
进行猴子补丁。
如果您尝试测试某些挂载操作,也许您可以让组件在挂载时使用其 props 调用一些独立函数,然后单独对该独立函数进行单元测试? 我会尽量避免对我想要测试的组件进行猴子修补,以便在它安装时测试它的整体行为。
从@mstijak和@jamespedid 继续,我再次强调,虽然您可能不鼓励“深度继承”(根据@sebmarkbage),但在某种意义上,能够在所有构建之后但在安装之前运行某些东西非常有用/重要那(这是打字稿):
// This is sanitised production code, sorry if the use case seems obscured.
abstract class BaseComponent<P> extends React.Component<P, {}> {
abstract key$: rx.Observable<string>
props$: rx.BehaviorSubject<P>
selected$: rx.Observable<any>
componentWillMount() {
this.props$ = new rx.BehaviorSubject(this.props)
this.selected$ = rx.Observable
.combineLatest(this.props$, this.key$)
.map(([props, key]) => props[key])
}
componentWillReceiveProps(nextProps: P) {
this.props$.next(nextProps)
}
}
class ConcreteComponent extends BaseComponent<{}> {
key$ = rx.Observable.of("name") // This actually changes in practice.
}
请注意, BaseComponent
不能在其构造函数内部执行初始化,因为它在子构造函数运行之前运行(因为子构造函数初始化key$
)
初始化不能在componentDidMount
内运行,因为初始渲染需要使用此处设置的属性(请注意,这不是异步的,因此将在 render() 之前准备好)。
这是一层继承(我们不会更深入)。 我猜,让您从组件中分离出公共部分的替代方法是 mixin? 另一种方法可能是使用额外的构造函数参数从子级传递每个这样的值(这有效,但非常不符合人体工程学(记住传递上下文!)并且需要父级初始化this.XXX
字段) .
所以总而言之,这种模式是稳定的,按原样正常工作,并且似乎没有令人愉快的替代品 AFAICT。 很高兴得到纠正。
@tekacs Yo dawg ,我听说你喜欢反应式编程
...所以我将您的props
放入 RxJS 流中,以便您在获得反应性更新时获得反应性更新
:眨眼:
@jedwards1211这只是为了从许多其他可观察值与props$
结合构建render$
可观察值,而不必专门处理后者。
但是,是的,我将一个组件作为一个整体进行被动更新,然后从内部对其进行被动更新。 尽管很愚蠢,但结果还是很不错的。 😆
render$ = obs.combineLatest(this.props$, this.data$, this.ready$).map(([props, data, ready]) => ...)
@tekacs就像在,你传递一些数据作为道具,一些来自 observables 代替 Redux,等等?
有一个用例我觉得很有说服力。 例如,您现在想触发异步数据请求,因为您知道很快就会需要它,但首先您需要呈现您的子内容。 有点像浏览器中的链接prefetch
。 但是,您不一定需要句柄和实例来执行此操作。
拥有一个不能访问this
或state
的静态方法可能就足够
static componentWillMount(props, context) { }
如果无法访问状态,您将如何存储异步请求的结果?
您将依靠外部系统来保持其缓存。 像 Relay 或一些 Flux 商店。 所以当你再次请求它时,它是可用的。 就像当您<link rel="prefetch" />
某些东西时您无法直接处理资源一样。 单独的请求获取数据。
请注意,此用例并不打算用作规范的数据加载模型。 这可以有一个基于#8595 中的想法的异步 API。 这将明确阻止在此子树上进一步渲染,直到获取数据。 您可以将这些组合起来,以便componentWillMount
预取,您呈现一些 API,然后在更新后阻止异步。
@tekacs无论如何,我认为有很多方法可以避免加深类层次结构,但仍然可以完成您想做的事情。 例如:
function createRxJSComponent({key$, methods}: {key$: rx.Observable<string>, methods?: Object}): ReactClass<P> {
class RxJSComponent extends React.Component<P, {}> {
props$: rx.BehaviorSubject<P>
selected$: rx.Observable<any>
componentWillMount() {
if (methods && methods.componentWillMount) methods.componentWillMount()
this.props$ = new rx.BehaviorSubject(this.props)
this.selected$ = rx.Observable
.combineLatest(this.props$, key$)
.map(([props, key]) => props[key])
}
componentWillReceiveProps(nextProps: P) {
if (methods && methods.componentWillReceiveProps) methods.componentWillReceiveProps(nextProps)
this.props$.next(nextProps)
}
}
for (let key in methods) {
if (!RxJSComponent[key]) RxJSComponent[key] = methods[key]
}
return RxJSComponent
}
const ConcreteComponent = createRxJSComponent({
key$: rx.Observable.of("name") // This actually changes in practice.
})
@jedwards1211 是的props$
+ 其他几个状态流,包括可观察的 GraphQL 查询(通过 Apollo)、状态存储和其他(例如Subject
s 传递给孩子以传播备份,而不是比传递onSomething
函数的混乱模式)。
您在上一篇文章中的建议确实有效(我当然已经想到了),只是我使用抽象类使我团队中的下游实现者可以轻松地:
a) 正确实现组件并且
b) 访问和覆盖许多此类组件使用的各种通用功能。
也许不太清楚我选择了我在帖子中找到的最简单的例子来减少事情,但我向你保证,如果使用上述模式来完成,结果会变得难以管理地混乱和巨大。 :使困惑:
一个实际的基本组件看起来更像这样(我最近在几个小时内写了这个,以便对一些核心进行排序,它有 10 多个子类):
https://gist.github.com/tekacs/d7f961479bd15cc238ce35dfb11fa403
虽然当然可以将其转换为您在上面给出的形式,但它几乎没有那么实用。
我的componentWillMount()
:
class Example extends React.PureComponent {
componentWillMount() {
const { fetchAction, fetchStatus, dispatch } = this.props;
if (fetchStatus !== FETCH_STATUSES.LOADING && fetchStatus !== FETCH_STATUSES.LOADED) {
dispatch(fetchAction);
}
}
render() { ... }
}
如果另一个组件呈现多个Example
组件,则dispatch(fetchAction)
仅按预期发生一次。 如果Example
组件使用componentDidMount()
,则dispatch(fetchAction)
被调用两次。 也许有更好的方法?
如果这已经被覆盖,我深表歉意。
@jpdesigndev我不知道为什么会这样; componentDidMount
只被调用一次,就像componentWillMount
?
对不起, @ljharb ,我不够清楚。
const SomeParent = (props) =>
<div>
<Example />
<Example />
</div>
如果SomeParent
组件渲染Example
组件两次(实际用例,我为通用示例道歉), componentDidMount()
将导致分派动作两次,而componentWillMount()
导致操作仅被分派一次(预期)。 我确信对此有一个完全合理的解释(同步/异步或其他一些时间原因),但这就是为什么我暂时更喜欢在componentWillMount()
进行这种条件数据获取。 也许这是一种反模式? 也许有人会建议我将数据提取移动到父级(这会破坏很好的封装)?
编辑:为了清楚起见(希望如此)
class Example extends React.PureComponent {
componentWillMount() {
const { fetchAction, fetchStatus, dispatch } = this.props;
//
// Notice the conditional here.
// If this code were in a componentDidMount(), the fetchStatus would be null
// for both <Example /> renders by <SomeParent />
//
// By utilizing componentWillMount()
// The second componentWillMount() call has the new fetchStatus
// that was updated synchronously by the dispatched action from the first <Example />
// rendered by <SomeParent />
if (fetchStatus !== FETCH_STATUSES.LOADING && fetchStatus !== FETCH_STATUSES.LOADED) {
dispatch(fetchAction);
}
}
render() { ... }
}
const SomeParent = (props) =>
<div>
<Example />
<Example />
</div>
这对我来说毫无意义——两个元素应该调用两个 WillMount。
@jpdesigndev我建议将数据从组件中提取出来,只保留数据提取请求在它们中分派。 参见 redux-saga。
@jpdesigndev抱歉,我的意思是检查提取是否正在进行并忽略请求的逻辑。
@jpdesigndev在您的示例中,不同之处在于componentWillMount
, this.props
指向尚未呈现的下一个道具。 而在componentDidMount
,它们指向刚刚渲染的道具。 这是因为didMount
在 React 的“提交阶段”被触发。 当您在didMount
调用setState
,React 直到当前提交阶段结束才会应用更新。 因此,即使您在第一个didMount
触发了dispatch
,但在您调用第二个didMount
该更新尚未应用。
一种解决方案是直接从商店读取FETCH_STATUS
,因为它在调度时同步变化。 而不是依赖于异步更新的 React props/state。
@ljharb @sompylasar @acdlite谢谢大家的想法/建议!
@sompylasar在组件级别保留此逻辑有时是有意义的。 例如,如果组件真的应该控制获取/重新获取。 在动作创建者上没有一些force
参数,它很简单,只需在组件中有条件地调用 fetch 而不是 Action Creator / saga / observable (epic) 控制何时获取。
@acdlite谢谢你的解释。 这确实有助于澄清我对为什么*WillMount
在这里比*DidMount
更有效的想法。 在这种情况下,我不确定我喜欢/理解直接从商店阅读的想法。 事实上,这是一个connect
ed 组件,它利用selectors
fetchStatus
从 props 中获取
我真的只是想在这里为componentWillMount()
布置一个用例(这似乎是有效的)。 我仍然不相信有更好的方法。
到目前为止,建议的更好/替代方法是:
fetchStatus
而不是依赖props
我完全愿意在这件事上改变主意。 也许,如果componentWillMount()
被弃用,我将被迫这样做。 人们是否同意这些方法中的任何一种都比我的componentWillMount()
示例更可取?
另外,我是否完全错过了这一点? 我是否应该已经在设计一种从componentWillMount()
迁移的方法?
@sompylasar在组件级别保留此逻辑有时是有意义的。 例如,如果组件真的应该控制获取/重新获取。 在动作创建器上没有一些强制参数,它很简单,只需在组件中有条件地调用 fetch 而不是 Action Creator / saga / observable (epic) 控制何时获取。
@jpdesigndev然后您有两个操作:“获取”(导致自动缓存且不重新获取)和“重新获取”(导致重置缓存、停止所有先前请求和重新获取)。
componentDidServerRender
现在有一个 RFC reactjs/rfcs/pull/8 😄
@sebmarkbage
目的是高度阻止副作用,因为它们具有不可预见的后果,即使您在技术上可以在构造函数中执行它们。
你能提供更多关于这方面的细节吗? 有没有边缘情况?
@NE-SmallTown 看看上面的链接
最有用的评论
@yaycmyk如果您可以使用字段初始值设定项,您会使用它吗?
或者更复杂的东西: