近几个月的工做中,有遇到一些场景:基本不须要全局的状态管理,但页面级的,确定须要在一些组件中共享,引入Redux这类状态管理库有点繁琐,直接经过props传递的话,写起来总以为不是那么优雅。恰好项目中React版本比较新,就试了下Context Api,代码大体以下:react
// Context.js
const Context = React.createContext(
{} // default value
)
export const Provider = Context.Provider
export const Consumer = Context.Consumer
复制代码
// App.jsx
import {Provider} from './Context'
import Page from './Page'
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
name: 'zz',
}
setName = name =>{
this.setState({name})
}
}
render() {
const val = {
...this.state,
setName: this.setName
}
return (
<Provider value={val}> <Page /> </Provider>
);
}
}
复制代码
// View.jsx
import React from 'react'
import {Consumer} from './Context'
export default class Page extends React.Component {
return (
<Consumer> { val => ( <button onClick={val.setName}> {val.name} </button> )} </Consumer>
);
}
复制代码
以上是官方文档中给出的用法,好处在于不用借助第三方状态管理库,也不须要手动传递props,但看起来不是很灵活,其实对于Provider和Consumer这种高阶组件,咱们能够借助decorators来简化写法,最后应该能到达一下这种效果:ios
// App.jsx
import React from 'react'
import { Provider } from './Context'
@Provider
export default class App extends React.Component{
// state 不写在这里,抽取到Context中
}
复制代码
// Page.jsx
import React from 'react'
import { Consumer } from './Context'
// 方法中传入须要map到props中的属性的key数组,若是不传,全部属性都会map
@Consumer(['list', 'query'])
export default class Page extends React.Component{
render(){
const { list, query } = this.props
return(
// ...
)
}
}
复制代码
能够看到这里的Provider和Consumer很简洁,固然这也并不是是Context中的Provider和Consumer,state状态的维护也抽离出去了,全部的这些逻辑是怎么实现的呢?先上代码:git
// Context.js
import React from 'react'
import service from './service'
const Context = React.createContext()
class ProviderClass extends React.Component {
constructor(props) {
super(props)
this.state = {
list: [],
keywords: null,
pagination: {
current: 1,
total: 0,
},
}
}
componentWillUnmount() {
this.unmount = true
}
update = state =>
new Promise((resolve, reject) => {
if (!this.unmount) {
this.setState(state, resolve)
}
})
query = () => {
const {
keywords,
pagination: { current },
} = this.state
service.query(current, keywords).then(({ count, pageNo, list }) => {
this.update({
list,
pagination: {
current: pageNo,
total: count,
},
})
})
}
search = keywords => this.update({ keywords }).then(this.query)
pageTo = (pageNo = 1) => {
this.update({
pagination: {
...this.pagination,
current: pageNo,
},
}).then(this.query)
}
render() {
const val = {
...this.state,
query: this.query,
pageTo: this.pageTo,
search: this.search,
}
return (
<Context.Provider value={val}>{this.props.children}</Context.Provider>
)
}
}
export const Provider => Comp => props => (
<ProviderClass>
<Comp {...props} />
</ProviderClass>
)
export const Consumer = keys => Comp => props => (
<Context.Consumer>
{val => {
let p = { ...props }
if (!keys) {
p = {
...p,
...val,
}
} else if (keys instanceof Array) {
keys.forEach(k => {
p[k] = val[k]
})
} else if (keys instanceof Function) {
p = {
...p,
...keys(val),
}
} else if (typeof keys === 'string') {
p[keys] = val[keys]
}
return <Comp {...p} />
}}
</Context.Consumer>
)
复制代码
这里已一个查询列表为例,这样封装了以后,无论是查询、翻页或者其余操做,页面上直接从props中取出来操做就行。ProviderClass中就是常规的操做state的逻辑,能够按照我的习惯来写。es6
Provider的封装也比较简单,但同时也能够很灵活,能够在前面再加个参数,好比type之类的,而后使用的时候:@Provider(type)
,总之,按本身的需求来写。github
看起来Consumer的实现稍微复杂点,其实作的事情很简单,就是处理@Consumer()
、@Consumer('name')
、@Consumer(['key1', 'key2'])
、@Consumer(val=>({name: val.name}))
这几种状况,毕竟想要更灵活嘛,并且,后面还实现了一种更灵活的Consumer.golang
这么写好像更复杂了啊,比以前的代码还要多,还要难以理解?但你应该也发现了,这个Context.js能够说是一个通用的,在不一样的场景,只须要实现ProviderClass中状态管理这部分就好了,而后就稍微把Provider和Consumer这两部分提取出来,写个module,之后直接import直接用就行了,一直这么想,可这几个月一直没时间去实现,每次都是yy / p
拷贝过来直接用。其实复制粘贴也没那么麻烦(ーー゛)。npm
最近终于有时间来总结一下了,此次实现了state的分离(直接写一个普通的es6 class就行),以及多Provider的场景,并且Provider、Consumer的使用更灵活了,废话很少说,直接来看一下最后的成果:axios
// Store.js
import axios from 'axios'
class Store {
userId = 00001
userName = zz
addr = {
province: 'Zhejiang',
city: 'Hangzhou'
}
login() {
axios.post('/login', {
// login data
}).then(({ userId, userName, prov, city }) => {
this.userId = userId
this.userName = userName
this.addr.province = prov
this.addr.city = city
})
}
}
export default new Store()
复制代码
// App.jsx
import React from 'react'
import {Provider} from 'ctx-react'
import store from './Store'
import Page from './Page'
@Provider(store)
export default class App extends React.Component {
render(){
return(
<Page /> ) } } 复制代码
// Page.jsx
import React from 'react'
import {Consumer} from 'ctx-react'
@Consumer
export default class Page extends React.Component {
render(){
const {userId, userName, addr:{province,city}, login} = this.props
return(
<div> <div className="user-id">{userId}</div> <div className="user-name">{userName}</div> <div className="addr-prov">{province}</div> <div className="addr-city">{city}</div> {/* form */} <button onClick={login}>Login</button> </div>
)
}
}
复制代码
而后,没有而后了,就是这么简单。固然,既然说了要灵活,那就必定是你想怎样就怎样。数组
// Provider中传入多个Store
@Provider(store1, store2, store3)
// Consumer 中只map须要的data和action到props中
@Consumer('name', 'setName')
// 再灵活一点?
@Consumer('userId',data => ({
prov: data.addr.provvince,
city: data.addr.city
}),'userName')
// 想要Multi Context ?
import { Provider, Consumer } from 'ctx-react' // 默认导出一个Provider和一个Consumer
import Context as {Context: Context1, Provider: Provider1} from 'ctx-react'
import Context as {Context: Context2, Provider: Provider2} from 'ctx-react'
// Store中有些数据不想要被代理,也不想传到Context中?
import { exclude } from 'ctx-react'
class Store{
name: 'zz',
@exclude temp: '这个字段不会进入到Context中'
}
复制代码
此次真的没了, 毕竟也就一百来行代码,还要啥自行车。不过存在的一些潜在问题仍是须要解决的,后续考虑加入scoop。ide
至于怎么实现的,其实大部分和上面对Context的封装差很少,对于state的抽离这部分稍微要注意点,用到了es6的Proxy, 在监听到set时触发更新,另外考虑到state中值为对象的状况,须要递归Proxy。
代码已丢到github,github.com/evolify/ctx…
也发到了npm:yarn add ctx-react
本觉得最近能闲下来玩一下golang,这篇文章还没写完就又忙起来了,算了算了,仍是先搬砖吧。