近幾個月的工做中,有遇到一些場景:基本不須要全局的狀態管理,但頁面級的,確定須要在一些組件中共享,引入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,這篇文章還沒寫完就又忙起來了,算了算了,仍是先搬磚吧。