? 近幾個(gè)月的工作中,有遇到一些場(chǎng)景:基本不需要全局的狀態(tài)管理霞扬,但頁(yè)面級(jí)的佑笋,肯定需要在一些組件中共享翼闹,引入Redux這類狀態(tài)管理庫(kù)有點(diǎn)繁瑣,直接通過(guò)props傳遞的話允青,寫(xiě)起來(lái)總覺(jué)得不是那么優(yōu)雅。剛好項(xiàng)目中React版本比較新,就試了下Context Api颠锉,代碼大致如下:
// 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>
);
}
? 以上是官方文檔中給出的用法法牲,好處在于不用借助第三方狀態(tài)管理庫(kù),也不需要手動(dòng)傳遞props琼掠,但看起來(lái)不是很靈活拒垃,其實(shí)對(duì)于Provider和Consumer這種高階組件,我們可以借助decorators來(lái)簡(jiǎn)化寫(xiě)法瓷蛙,最后應(yīng)該能到達(dá)一下這種效果:
// App.jsx
import React from 'react'
import { Provider } from './Context'
@Provider
export default class App extends React.Component{
// state 不寫(xiě)在這里悼瓮,抽取到Context中
}
// Page.jsx
import React from 'react'
import { Consumer } from './Context'
// 方法中傳入需要map到props中的屬性的key數(shù)組,如果不傳艰猬,所有屬性都會(huì)map
@Consumer(['list', 'query'])
export default class Page extends React.Component{
render(){
const { list, query } = this.props
return(
// ...
)
}
}
? 可以看到這里的Provider和Consumer很簡(jiǎn)潔横堡,當(dāng)然這也并非是Context中的Provider和Consumer,state狀態(tài)的維護(hù)也抽離出去了冠桃,所有的這些邏輯是怎么實(shí)現(xiàn)的呢命贴?先上代碼:
// 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>
)
? 這里已一個(gè)查詢列表為例,這樣封裝了之后食听,不管是查詢胸蛛、翻頁(yè)或者其他操作,頁(yè)面上直接從props中取出來(lái)操作就行樱报。ProviderClass中就是常規(guī)的操作state的邏輯葬项,可以按照個(gè)人習(xí)慣來(lái)寫(xiě)。
? Provider的封裝也比較簡(jiǎn)單迹蛤,但同時(shí)也可以很靈活民珍,可以在前面再加個(gè)參數(shù),比如type之類的笤受,然后使用的時(shí)候:@Provider(type)
穷缤,總之,按自己的需求來(lái)寫(xiě)箩兽。
? 看起來(lái)Consumer的實(shí)現(xiàn)稍微復(fù)雜點(diǎn)津肛,其實(shí)做的事情很簡(jiǎn)單,就是處理@Consumer()
汗贫、@Consumer('name')
身坐、@Consumer(['key1', 'key2'])
、@Consumer(val=>({name: val.name}))
這幾種情況落包,畢竟想要更靈活嘛部蛇,而且,后面還實(shí)現(xiàn)了一種更靈活的Consumer.
? 這么寫(xiě)好像更復(fù)雜了啊咐蝇,比之前的代碼還要多涯鲁,還要難以理解?但你應(yīng)該也發(fā)現(xiàn)了,這個(gè)Context.js可以說(shuō)是一個(gè)通用的抹腿,在不同的場(chǎng)景岛请,只需要實(shí)現(xiàn)ProviderClass中狀態(tài)管理這部分就行了,然后就稍微把Provider和Consumer這兩部分提取出來(lái)警绩,寫(xiě)個(gè)module崇败,以后直接import直接用就好了,一直這么想肩祥,可這幾個(gè)月一直沒(méi)時(shí)間去實(shí)現(xiàn)后室,每次都是yy / p
拷貝過(guò)來(lái)直接用。其實(shí)復(fù)制粘貼也沒(méi)那么麻煩(ーー゛)混狠。
? 最近終于有時(shí)間來(lái)總結(jié)一下了岸霹,這次實(shí)現(xiàn)了state的分離(直接寫(xiě)一個(gè)普通的es6 class就行),以及多Provider的場(chǎng)景檀蹋,而且Provider松申、Consumer的使用更靈活了,廢話不多說(shuō)俯逾,直接來(lái)看一下最后的成果:
// 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>
)
}
}
? 然后贸桶,沒(méi)有然后了,就是這么簡(jiǎn)單桌肴。當(dāng)然皇筛,既然說(shuō)了要靈活,那就一定是你想怎樣就怎樣坠七。
// Provider中傳入多個(gè)Store
@Provider(store1, store2, store3)
// Consumer 中只map需要的data和action到props中
@Consumer('name', 'setName')
// 再靈活一點(diǎn)水醋?
@Consumer('userId',data => ({
prov: data.addr.provvince,
city: data.addr.city
}),'userName')
// 想要Multi Context ?
import { Provider, Consumer } from 'ctx-react' // 默認(rèn)導(dǎo)出一個(gè)Provider和一個(gè)Consumer
import Context as {Context: Context1, Provider: Provider1} from 'ctx-react'
import Context as {Context: Context2, Provider: Provider2} from 'ctx-react'
// Store中有些數(shù)據(jù)不想要被代理,也不想傳到Context中彪置?
import { exclude } from 'ctx-react'
class Store{
name: 'zz',
@exclude temp: '這個(gè)字段不會(huì)進(jìn)入到Context中'
}
? 這次真的沒(méi)了, 畢竟也就一百來(lái)行代碼拄踪,還要啥自行車。不過(guò)存在的一些潛在問(wèn)題還是需要解決的拳魁,后續(xù)考慮加入scoop惶桐。
? 至于怎么實(shí)現(xiàn)的,其實(shí)大部分和上面對(duì)Context的封裝差不多潘懊,對(duì)于state的抽離這部分稍微要注意點(diǎn)姚糊,用到了es6的Proxy, 在監(jiān)聽(tīng)到set時(shí)觸發(fā)更新,另外考慮到state中值為對(duì)象的情況授舟,需要遞歸Proxy救恨。
? 代碼已丟到github,https://github.com/evolify/ctx-react
? 也發(fā)到了npm:yarn add ctx-react
? 本以為最近能閑下來(lái)玩一下golang释树,這篇文章還沒(méi)寫(xiě)完就又忙起來(lái)了肠槽,算了算了擎淤,還是先搬磚吧。