react 全局状态
by Diego Haz
迭戈·哈兹(Diego Haz)
React的新上下文API:在本地状态和全局状态之间切换 (React's new context API: toggle between local and global state)
Consider a component that handles a visibility state and passes it down to its children via render props:
考虑一个处理可见性状态并将其通过render props传递给其子级的组件:
const PopoverContainer = () => ( <VisibilityContainer> {({ toggle, hidden }) => ( <div> <button onClick={toggle}>PopoverButton</button> <div hidden={hidden}>PopoverContent</div> </div> )} </VisibilityContainer>);
What would you think about being able to make that state global by just changing a context
property on the component?
您仅通过更改组件上的context
属性就可以使该状态变为全局的想法如何?
const PopoverButton = () => ( <VisibilityContainer context="popover1"> {({ toggle }) => ( <button onClick={toggle}>PopoverButton</button> )} </VisibilityContainer>);
const PopoverContent = () => ( <VisibilityContainer context="popover1"> {({ hidden }) => ( <div hidden={hidden}>PopoverContent</div> )} </VisibilityContainer>);
That's what we're going to achieve in this article.
这就是我们在本文中要实现的目标。
语境与状态 (Context and State)
First, before talking about context and state in React, let me give you some context on the state of this topic (!).
首先,在讨论React中的上下文和状态之前,让我为您提供有关此主题状态的上下文 (!)。
Some months ago I published reas, an experimental UI toolkit powered by React and styled-components.
几个月前,我发布了reas ,这是一个由React和styled-components支持的实验性UI工具包。
Besides components themselves, I wanted to provide helpers to handle their state. The approach I took at that time was to export some high-order components (HOCs), such as withPopoverContainer
, so as to control the visibility state of a Popover
component. Take a look at this example:
除了组件本身,我还想提供一些帮助程序来处理它们的状态。 我当时采用的方法是导出一些高阶组件 ( withPopoverContainer
),例如withPopoverContainer
,以便控制Popover
组件的可见性状态。 看一下这个例子:
import { Popover, withPopoverContainer } from "reas";
const MyComponent = ({ toggle, visible }) => ( <div> <button onClick={toggle}>Toggle</button> <Popover visible={visible}>Popover</Popover> </div>);
export default withPopoverContainer(MyComponent);
But HOCs have some problems, such as name collision. What if another HOC or a parent component passes its own toggle
prop to MyComponent
? Things will certainly break.
但是HOC存在一些问题,例如名称冲突。 如果另一个HOC或父组件将其自身的toggle
道具传递给MyComponent
怎么办? 事情肯定会破裂。
Even before that, inspired by Michael Jackson and his great talk, the React community started to adopt render props over HOCs.
甚至在此之前,受迈克尔·杰克逊 ( Michael Jackson)及其精彩演讲的启发,React社区开始在HOC上采用渲染道具 。
Also, React v16.3.0 introduced a new context API, replacing the old unstable one, using render props.
另外,React v16.3.0引入了新的上下文API ,使用渲染道具代替了旧的不稳定 API 。
I've learned to look at all that stuff that gets hyped up, especially the stuff brought up by the JavaScript community, with a critical eye. This keeps my mind sane and prevents me from having to refactor my code every single day with cool new libraries.
我学会了用批判性的眼光看待所有被炒作的东西,尤其是JavaScript社区提出的东西。 这使我头脑清醒,并使我不必每天都使用很棒的新库来重构代码。
Finally, I posted a tweet asking people which they prefer: render props or HOCs. All comments were favorable to render props, which eventually made me turn all HOCs in reas into components with render props:
最后,我发布了一条推文,询问人们更喜欢哪些:渲染道具或HOC。 所有评论都对渲染道具有利,最终使我将所有HOC变成了带有渲染道具的组件:
import { Popover } from "reas";
const MyComponent = () => ( <Popover.Container> {({ toggle, visible }) => ( <div> <button onClick={toggle}>Toggle</button> <Popover visible={visible}>Popover</Popover> </div> )} </Popover.Container>);
export default MyComponent;
Popover.Container
was a regular React component class with a toggle
method using this.setState
to change this.state.visible
. Simple as that.
Popover.Container
是常规的React组件类,带有使用this.setState
更改this.state.visible
的toggle
方法。 就那么简单。
It was good and worked pretty well. However, in one of my projects I had a button
that was supposed to control the Popover
component placed in a completely different path in the React tree.
很好,并且运作良好。 但是,在我的一个项目中,我有一个button
应该用来控制Popover
组件,该组件放置在React树中的完全不同的路径中。
I either needed to have some sort of global state manager like Redux, or I needed to move Popover.Container
up in the tree in a common parent and pass the props down until they touched both button
and Popover
. But this sounded like a terrible idea.
我要么需要像Redux这样的全局状态管理器,要么需要将Popover.Container
在同一个父级的树中上移, Popover.Container
下传递这些道具,直到它们同时碰到button
和Popover
。 但这听起来像一个可怕的主意。
Also, setting up Redux and rewriting all the logic I already had with this.setState
into actions and reducers just to have that functionality would be an awful job.
另外,设置Redux并将我已经拥有的this.setState
逻辑全部重写为动作和化this.setState
,以具有该功能将是一项this.setState
的工作。
I think this imminent need of shared state is one of the reasons why people prematurely optimize their apps. That is, setting up all the libraries they might need up front, which includes a global state management library.
我认为对共享状态的这种迫切需求是人们过早优化其应用程序的原因之一。 也就是说,预先设置他们可能需要的所有库,其中包括全局状态管理库。
React's new context API comes in handy to solve this issue. I wanted to keep using regular React local state and only scale up to global state when needed, without needing to rewrite my state logic. That's why I built constate.
React的新上下文API可以方便地解决此问题。 我想继续使用常规的React局部状态,并且仅在需要时才扩展到全局状态,而无需重写我的状态逻辑。 这就是为什么我建州立 。
州立 (Constate)
Let's see how PopoverContainer
would look with constate:
让我们看看如何PopoverContainer
看起来与constate :
import React from "react";import { Container } from "constate";
const PopoverContainer = props => ( <Container initialState={{ visible: false }} actions={{ toggle: () => state =>; ({ visible: !state.visible }) }} {...props} />);
export default PopoverContainer;
Now we can wrap our component with PopoverContainer
so as to have access to visible
and toggle
members already passed by Container
to the children
function as an argument.
现在,我们可以换我们的组件PopoverContainer
以便具有访问visible
,并toggle
已经过去的成员Container
的children
作为参数的功能。
Also, note that we are passing all props received from PopoverContainer
to Container
. This means that we can compose it to create a new derived state component, such as AdvancedPopoverContainer
, with new initialState
and actions
.
另外,请注意,我们PopoverContainer
所有从PopoverContainer
收到的道具传递给Container
。 这意味着我们可以对其进行组合,以使用新的initialState
和actions
创建一个新的派生状态组件,例如AdvancedPopoverContainer
。
引擎盖下 (Under the hood)
If you're like me, and you like to know how things were implemented under the hood, you're probably thinking about how Container
was implemented. So, let's recreate a simple Container
component:
如果您像我一样,并且想知道事情是如何实现的,那么您可能正在考虑如何实现Container
。 因此,让我们重新创建一个简单的Container
组件:
import React from "react";
class Container extends React.Component { state = this.props.initialState;
render() { return this.props.children({ ...this.state, ...mapStateToActions(...) }); }}
export default Container;
mapStateToActions
is a utility function that passes state to each member of actions
. That's what makes it possible to define our toggle
function like this:
mapStateToActions
是一个实用程序函数,它将状态传递给每个actions
成员。 这就是可以这样定义我们的toggle
功能的原因:
const actions = { toggle: () => state => ({ visible: !state.visible})};
Our goal, however, is to be able to use the same PopoverContainer
as a global state. With constate we just need to pass a context
prop to Container
:
但是,我们的目标是能够使用与全局状态相同的PopoverContainer
。 有了constate,我们只需要向Container
传递一个context
支持:
<PopoverContainer context="popover1"> {({ toggle }) => ( <button onClick={toggle}>PopoverToggle</button> )}</PopoverContainer>
Now, every Container
with context="popover1"
will share the same state.
现在,每个具有context="popover1"
Container
将共享相同的状态。
Of course, you're curious about how Container
handles that context
prop. So here you go:
当然,您对Container
如何处理该context
道具感到好奇。 因此,您在这里:
import React from "react";import Consumer from "./Consumer";
class Container extends React.Component { state = this.props.initialState;
render() { if (this.props.context) { return <Consumer {...this.props} />; }
return this.props.children({ ...this.state, ...mapStateToActions(...) }); }}
export default Container;
Ok, I'm sorry. Those four added lines don't tell you much. To create Consumer
, we need to understand how to deal with the new React Context API.
好吧,我很抱歉。 这四行添加的内容并没有告诉您太多。 要创建Consumer
,我们需要了解如何处理新的React Context API。
React上下文 (React Context)
We can break the new React Context API into three parts: Context
, Provider
and Consumer
.
我们可以将新的React Context API分为三个部分: Context
, Provider
和Consumer
。
Let's create the context:
让我们创建上下文:
import React from "react";
const Context = React.createContext();
export default Context;
Then, we create our Provider
, which uses Context.Provider
and passes state
and setState
down:
然后,我们创建Provider
,它使用Context.Provider
并向下传递state
和setState
:
import React from "react";import Context from "./Context";
class Provider extends React.Component { handleSetState = fn => { this.setState(state => ({ state: fn(state.state) })); };
state = { state: this.props.initialState, setState: this.handleSetState };
render() { return ( <Context.Provider value={this.state}> {this.props.children} </Context.Provider> ); }}
export default Provider;
It can be a little tricky. We can't simply pass { state, setState }
as a literal object to Context.Provider
's value
since it would recreate that object on every render. Learn more here.
这可能有点棘手。 我们不能简单地将{ state, setState }
作为文字对象传递给Context.Provider
的value
因为它会在每个渲染器上重新创建该对象。 在这里了解更多。
Finally, our Consumer
needs to use Context.Consumer
to access state
and setState
passed by Provider
:
最后,我们的Consumer
需要使用Context.Consumer
来访问Provider
传递的state
和setState
:
import React from "react";import Context from "./Context";
const Consumer = ({ context, children, actions }) => ( <Context.Consumer> {({ state, setState }) => children({ ...state[context], ...mapContextToActions(...) })} </Context.Consumer>);
export default Consumer;
mapContextToActions
is similar to mapStateToActions
. The difference is that the former maps state[context]
instead of just state
.
mapContextToActions
与mapStateToActions
相似。 不同之处在于,前者映射state[context]
而不是state
。
The final step is to wrap our app with Provider
:
最后一步是使用Provider
包装我们的应用Provider
:
import React from "react";import ReactDOM from "react-dom";import Provider from "./Provider";
const App = () => ( <Provider> ... </Provider>);
ReactDOM.render(<App />, document.getElementById("root"));
Finally, we have rewritten constate. Now you can use Container
component to switch between local and global state with ease.
最后,我们重写了constate 。 现在,您可以使用Container
组件轻松地在本地状态和全局状态之间切换。
结论 (Conclusion)
You might be thinking that starting a project with something like constate could also be a premature optimization. And you're probably right. You should stick with this.setState
without abstractions as long as you can.
您可能会认为,使用constate之类的项目启动项目可能还为时过早。 你可能是对的。 您应该尽可能地坚持this.setState
而不进行抽象。
However, not all premature optimizations are the root of all evil. You should find a good balance between simplicity and scalability. That is, you should pursue simple implementations, specially if you're building small applications. But, if you're planning to grow, you should look for simple implementations that are also easy to scale.
但是,并非所有过早的优化都是万恶之源 。 您应该在简单性和可伸缩性之间找到良好的平衡。 也就是说,您应该追求简单的实现,尤其是在构建小型应用程序时。 但是,如果您打算增长,则应该寻找易于扩展的简单实现。
谢谢您阅读此篇! (Thank you for reading this!)
If you like it and find it useful, here are some things you can do to show your support:
如果您喜欢它并觉得有用,则可以执行以下操作以显示您的支持:
- Hit the clap ? button on this article a few times (up to 50) 打拍手? 在本文上单击几次(最多50次)
Give a star ⭐️ on GitHub: https://github.com/diegohaz/constate
在GitHub上给一个星星⭐️: https : //github.com/diegohaz/constate
Follow me on GitHub: https://github.com/diegohaz
在GitHub上关注我: https : //github.com/diegohaz
Follow me on Twitter: https://twitter.com/diegohaz
在Twitter上关注我: https : //twitter.com/diegohaz
react 全局状态