使用 React 的上下文允许将 child 组件呈现为 parent/grandparent/great-grandparent... Anti-pattern?
Using react's context to allow rendering of a child component into a parent/grandparent/great-grandparent.... Anti-pattern?
我有一个概念证明,它似乎有效,但我的一部分想知道这是否真的是个好主意,是否有更好的解决方案,比如使用 Redux 或替代方案策略。
问题
基本上,我的整个应用程序都有一个基本的 React 组件,它有一堆您可能期望的典型组件,header、菜单、页脚等。
在我的树下(更远),我有一个组件,如果我可以在我的 header 组件中安装一个新的菜单项,那将是非常棒的。 header 组件当然位于我的应用程序的顶部,因此访问被拒绝。
这只是一个这样的例子,但这是我从多个角度遇到的问题案例。
我的疯狂解决方案
我研究了使用 React 的上下文,以便公开允许 child 组件声明它们希望出现在 header 中的任何其他元素的函数。
在尝试了这个概念之后,我最终将其重构为一个非常通用的解决方案,它本质上是一个 React Element 消息传递系统。此解决方案分为三个部分。
1.供应商
单实例组件与 Redux 的 Connect 组件非常相似。她本质上是接收和传递消息的引擎。她的基本结构(以上下文为中心)是:
class ElementInjectorProvider extends Component {
childContextTypes: {
// :: (namespace, [element]) -> void
produceElements: PropTypes.func.isRequired,
// :: (namespace, [element]) -> void
removeElements: PropTypes.func.isRequired,
// :: (listener, namespace, ([element]) -> void) -> void
consumeElements: PropTypes.func.isRequired,
// :: (listener) -> void
stopConsumingElements: PropTypes.func.isRequired,
}
/* ... Implementation ... */
}
2。制片人
高阶组件。每个实例可以通过 produceElements
上下文项 "produce" 元素,为特定命名空间提供元素,然后通过 removeElements
.
删除元素(在组件卸载的情况下)
function ElementInjectorProducer(config) {
const { namespace } = config;
return function WrapComponent(WrappedComponent) {
class ElementInjectorConsumerComponent {
contextTypes = {
produceElements: PropTypes.func.isRequired,
removeElements: PropTypes.func.isRequired
}
/* ... Implementation ... */
}
return ElementInjectorProducerComponent;
};
}
3。消费者
高阶组件。对于附加到给定名称空间的元素,每个实例都配置为 "watch"。它使用 consumeElements
到 "start" 通过回调函数注册的侦听和 stopConsumingElements
取消注册消费。
function ElementInjectorConsumer(config) {
const { namespace } = config;
return function WrapComponent(WrappedComponent) {
class ElementInjectorConsumerComponent {
contextTypes = {
consumeElements: PropTypes.func.isRequired,
stopConsumingElements: PropTypes.func.isRequired
}
/* ... Implementation ... */
}
return ElementInjectorConsumerComponent;
};
}
这是我打算做的事情的粗略概述。从本质上看,它是一个消息传递系统。也许可以进一步抽象。
我已经在玩 redux,猜猜 Redux 有什么用?所以我不禁觉得,虽然这对我有用,但也许这不是一个好的设计,我无意中站在了 Redux 的脚趾上,或者产生了一个通用的 anti-pattern.
我想我没有直接跳到使用 Redux 的唯一原因是因为我正在生成元素,而不是简单状态。我可以沿着创建元素描述符 objects 的路线前进,然后通过 Redux 将其传递下去,但这本身就很复杂。
有什么妙语吗?
第 1 条更新
对上述内容进行一些补充说明。
这让我可以在我的完整组件树上向上和向下,甚至从左到右注入元素。我知道大多数 React Context 示例都描述了将数据从祖父母注入到祖父母 child 组件中。
此外,我希望上述实现能够从开发人员那里抽象出任何有关 Context 使用的知识。事实上,我很可能会使用这些 HOFS 来创建额外的包装器,这些包装器特定于用例并且更加明确。
即
消费者实现:
<InjectableHeader />
生产者实施:
InjectIntoHeader(<FooButton />)(FooPage)
我认为它非常明确并且易于理解。我确实喜欢我可以在它最关心的地方创建按钮,这使我能够与它的同行建立更牢固的关系。
我也知道 redux flow 可能是正确的想法。感觉就像我让自己变得更难了 - 我忍不住认为这种技术可能有一些优点。
为什么这是一个特别糟糕的主意?
更新 2
好的,我现在确信这是个坏主意。我基本上打破了应用程序的可预测性并使 uni-directional 数据模型提供的所有好处无效。
我仍然不相信使用 Redux 是这种情况下的最佳解决方案,我想出了一个更明确的 uni-directional 解决方案,它使用了上面的一些概念,但没有任何上下文魔法.
如果我认为可行,我会post任何解决方案作为答案。如果做不到这一点,我会去 Redux 并因为没有早点听你们的而自责。
其他例子
以下是其他一些 projects/ideas 尝试使用各种技术解决相同(差不多)问题的方法:
https://joecritchley.svbtle.com/portals-in-reactjs
我对何时使用 redux/flux/reflux/anothingelseux 以及何时使用上下文的想法:
- -ux 存储对于以横向方式存储组件之间共享的信息很有用。这通常是您的用例:在没有任何其他明显联系且在树中彼此远离的组件之间进行通信。
- 当您不知道他们会在哪里或有多少人需要他们时,上下文可用于向 children 提供信息。例如,我使用地图的上下文为当前视口上的 children 提供信息。我不知道是否所有 children 都会使用它,但他们都可能对此感兴趣并且不应该更改它。
我会说在你的情况下,-ux 方式是可行的,你的包装器组件没有理由处理与其无关的逻辑,而且代码会很晦涩。想象一下,开发人员稍后会查看您的代码,并看到您通过上下文接收到它。任何 parent 都可以发送它,因此他需要检查它的发送位置。您的包装器组件也会发生同样的情况,如果类似的操作开始成倍增加,您将处理其中许多与那里无关的方法和处理程序。
拥有一个带有 action 和 reducers 的 store 可以分离你的案例中的关注点,这将是最易读的做事方式。
好的,所以,智慧可能说使用 Redux 和它的单向数据流是最好的解决方案。因此,我将@Mijamo 的答案设置为答案。
我最终创建了我在 post 中谈论的注射剂解决方案。到目前为止,它一直非常有用。它实际上真的非常有用,我已经能够制作出一些使用其他技术会太复杂的很棒的东西。
最棒的是我的可注入目标不需要明确知道将注入其中的每个可能的组件。
我对自己所做的事情非常满意,因此我创建了一个库:
https://github.com/ctrlplusb/react-injectables
如您所见,我尝试使组件绑定 (target/source) 尽可能明确。您必须实际导入并绑定代码中的所有相应目标。这很有用,因为您可以获得 target/source 绑定的编译时间检查(以及排序)。比基于魔术字符串的绑定更有用。
无论如何,这可能仍然是一个疯狂的想法,但也许我疯了,这就是我如此喜欢它的原因。 :)
我有一个概念证明,它似乎有效,但我的一部分想知道这是否真的是个好主意,是否有更好的解决方案,比如使用 Redux 或替代方案策略。
问题
基本上,我的整个应用程序都有一个基本的 React 组件,它有一堆您可能期望的典型组件,header、菜单、页脚等。
在我的树下(更远),我有一个组件,如果我可以在我的 header 组件中安装一个新的菜单项,那将是非常棒的。 header 组件当然位于我的应用程序的顶部,因此访问被拒绝。
这只是一个这样的例子,但这是我从多个角度遇到的问题案例。
我的疯狂解决方案
我研究了使用 React 的上下文,以便公开允许 child 组件声明它们希望出现在 header 中的任何其他元素的函数。
在尝试了这个概念之后,我最终将其重构为一个非常通用的解决方案,它本质上是一个 React Element 消息传递系统。此解决方案分为三个部分。
1.供应商
单实例组件与 Redux 的 Connect 组件非常相似。她本质上是接收和传递消息的引擎。她的基本结构(以上下文为中心)是:
class ElementInjectorProvider extends Component {
childContextTypes: {
// :: (namespace, [element]) -> void
produceElements: PropTypes.func.isRequired,
// :: (namespace, [element]) -> void
removeElements: PropTypes.func.isRequired,
// :: (listener, namespace, ([element]) -> void) -> void
consumeElements: PropTypes.func.isRequired,
// :: (listener) -> void
stopConsumingElements: PropTypes.func.isRequired,
}
/* ... Implementation ... */
}
2。制片人
高阶组件。每个实例可以通过 produceElements
上下文项 "produce" 元素,为特定命名空间提供元素,然后通过 removeElements
.
function ElementInjectorProducer(config) {
const { namespace } = config;
return function WrapComponent(WrappedComponent) {
class ElementInjectorConsumerComponent {
contextTypes = {
produceElements: PropTypes.func.isRequired,
removeElements: PropTypes.func.isRequired
}
/* ... Implementation ... */
}
return ElementInjectorProducerComponent;
};
}
3。消费者
高阶组件。对于附加到给定名称空间的元素,每个实例都配置为 "watch"。它使用 consumeElements
到 "start" 通过回调函数注册的侦听和 stopConsumingElements
取消注册消费。
function ElementInjectorConsumer(config) {
const { namespace } = config;
return function WrapComponent(WrappedComponent) {
class ElementInjectorConsumerComponent {
contextTypes = {
consumeElements: PropTypes.func.isRequired,
stopConsumingElements: PropTypes.func.isRequired
}
/* ... Implementation ... */
}
return ElementInjectorConsumerComponent;
};
}
这是我打算做的事情的粗略概述。从本质上看,它是一个消息传递系统。也许可以进一步抽象。
我已经在玩 redux,猜猜 Redux 有什么用?所以我不禁觉得,虽然这对我有用,但也许这不是一个好的设计,我无意中站在了 Redux 的脚趾上,或者产生了一个通用的 anti-pattern.
我想我没有直接跳到使用 Redux 的唯一原因是因为我正在生成元素,而不是简单状态。我可以沿着创建元素描述符 objects 的路线前进,然后通过 Redux 将其传递下去,但这本身就很复杂。
有什么妙语吗?
第 1 条更新
对上述内容进行一些补充说明。
这让我可以在我的完整组件树上向上和向下,甚至从左到右注入元素。我知道大多数 React Context 示例都描述了将数据从祖父母注入到祖父母 child 组件中。
此外,我希望上述实现能够从开发人员那里抽象出任何有关 Context 使用的知识。事实上,我很可能会使用这些 HOFS 来创建额外的包装器,这些包装器特定于用例并且更加明确。
即
消费者实现:
<InjectableHeader />
生产者实施:
InjectIntoHeader(<FooButton />)(FooPage)
我认为它非常明确并且易于理解。我确实喜欢我可以在它最关心的地方创建按钮,这使我能够与它的同行建立更牢固的关系。
我也知道 redux flow 可能是正确的想法。感觉就像我让自己变得更难了 - 我忍不住认为这种技术可能有一些优点。
为什么这是一个特别糟糕的主意?
更新 2
好的,我现在确信这是个坏主意。我基本上打破了应用程序的可预测性并使 uni-directional 数据模型提供的所有好处无效。
我仍然不相信使用 Redux 是这种情况下的最佳解决方案,我想出了一个更明确的 uni-directional 解决方案,它使用了上面的一些概念,但没有任何上下文魔法.
如果我认为可行,我会post任何解决方案作为答案。如果做不到这一点,我会去 Redux 并因为没有早点听你们的而自责。
其他例子
以下是其他一些 projects/ideas 尝试使用各种技术解决相同(差不多)问题的方法:
https://joecritchley.svbtle.com/portals-in-reactjs
我对何时使用 redux/flux/reflux/anothingelseux 以及何时使用上下文的想法:
- -ux 存储对于以横向方式存储组件之间共享的信息很有用。这通常是您的用例:在没有任何其他明显联系且在树中彼此远离的组件之间进行通信。
- 当您不知道他们会在哪里或有多少人需要他们时,上下文可用于向 children 提供信息。例如,我使用地图的上下文为当前视口上的 children 提供信息。我不知道是否所有 children 都会使用它,但他们都可能对此感兴趣并且不应该更改它。
我会说在你的情况下,-ux 方式是可行的,你的包装器组件没有理由处理与其无关的逻辑,而且代码会很晦涩。想象一下,开发人员稍后会查看您的代码,并看到您通过上下文接收到它。任何 parent 都可以发送它,因此他需要检查它的发送位置。您的包装器组件也会发生同样的情况,如果类似的操作开始成倍增加,您将处理其中许多与那里无关的方法和处理程序。
拥有一个带有 action 和 reducers 的 store 可以分离你的案例中的关注点,这将是最易读的做事方式。
好的,所以,智慧可能说使用 Redux 和它的单向数据流是最好的解决方案。因此,我将@Mijamo 的答案设置为答案。
我最终创建了我在 post 中谈论的注射剂解决方案。到目前为止,它一直非常有用。它实际上真的非常有用,我已经能够制作出一些使用其他技术会太复杂的很棒的东西。
最棒的是我的可注入目标不需要明确知道将注入其中的每个可能的组件。
我对自己所做的事情非常满意,因此我创建了一个库:
https://github.com/ctrlplusb/react-injectables
如您所见,我尝试使组件绑定 (target/source) 尽可能明确。您必须实际导入并绑定代码中的所有相应目标。这很有用,因为您可以获得 target/source 绑定的编译时间检查(以及排序)。比基于魔术字符串的绑定更有用。
无论如何,这可能仍然是一个疯狂的想法,但也许我疯了,这就是我如此喜欢它的原因。 :)