基于React Context Api 和 Es6 Proxy的狀態(tài)管理

? 近幾個(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)了肠槽,算了算了擎淤,還是先搬磚吧。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末秸仙,一起剝皮案震驚了整個(gè)濱河市揉燃,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌筋栋,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件正驻,死亡現(xiàn)場(chǎng)離奇詭異弊攘,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)姑曙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)襟交,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人伤靠,你說(shuō)我怎么就攤上這事捣域。” “怎么了宴合?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵焕梅,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我卦洽,道長(zhǎng)贞言,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任阀蒂,我火速辦了婚禮该窗,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蚤霞。我一直安慰自己酗失,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布昧绣。 她就那樣靜靜地躺著规肴,像睡著了一般。 火紅的嫁衣襯著肌膚如雪滞乙。 梳的紋絲不亂的頭發(fā)上奏纪,一...
    開(kāi)封第一講書(shū)人閱讀 49,144評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音斩启,去河邊找鬼序调。 笑死,一個(gè)胖子當(dāng)著我的面吹牛兔簇,可吹牛的內(nèi)容都是我干的发绢。 我是一名探鬼主播硬耍,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼边酒!你這毒婦竟也來(lái)了经柴?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤墩朦,失蹤者是張志新(化名)和其女友劉穎坯认,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體氓涣,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡牛哺,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了劳吠。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片引润。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖痒玩,靈堂內(nèi)的尸體忽然破棺而出淳附,到底是詐尸還是另有隱情,我是刑警寧澤蠢古,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布奴曙,位于F島的核電站,受9級(jí)特大地震影響草讶,放射性物質(zhì)發(fā)生泄漏缆毁。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一到涂、第九天 我趴在偏房一處隱蔽的房頂上張望脊框。 院中可真熱鬧,春花似錦践啄、人聲如沸浇雹。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)昭灵。三九已至,卻和暖如春伐谈,著一層夾襖步出監(jiān)牢的瞬間烂完,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工诵棵, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留抠蚣,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓履澳,卻偏偏與公主長(zhǎng)得像嘶窄,于是被迫代替她去往敵國(guó)和親怀跛。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容