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

? 近幾個月的工作中,有遇到一些場景:基本不需要全局的狀態(tài)管理,但頁面級的,肯定需要在一些組件中共享,引入Redux這類狀態(tài)管理庫有點繁瑣,直接通過props傳遞的話,寫起來總覺得不是那么優(yōu)雅。剛好項目中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)管理庫,也不需要手動傳遞props,但看起來不是很靈活,其實對于Provider和Consumer這種高階組件,我們可以借助decorators來簡化寫法,最后應該能到達一下這種效果:

// 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數(shù)組,如果不傳,所有屬性都會map
@Consumer(['list', 'query'])
export default class Page extends React.Component{
    render(){
        const { list, query } = this.props
        return(
            // ...
        )
    }
}

? 可以看到這里的Provider和Consumer很簡潔,當然這也并非是Context中的Provider和Consumer,state狀態(tài)的維護也抽離出去了,所有的這些邏輯是怎么實現(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>
)

? 這里已一個查詢列表為例,這樣封裝了之后,不管是查詢、翻頁或者其他操作,頁面上直接從props中取出來操作就行。ProviderClass中就是常規(guī)的操作state的邏輯,可以按照個人習慣來寫。

? Provider的封裝也比較簡單,但同時也可以很靈活,可以在前面再加個參數(shù),比如type之類的,然后使用的時候:@Provider(type),總之,按自己的需求來寫。

? 看起來Consumer的實現(xiàn)稍微復雜點,其實做的事情很簡單,就是處理@Consumer()、@Consumer('name') 、@Consumer(['key1', 'key2'])、@Consumer(val=>({name: val.name}))這幾種情況,畢竟想要更靈活嘛,而且,后面還實現(xiàn)了一種更靈活的Consumer.

? 這么寫好像更復雜了啊,比之前的代碼還要多,還要難以理解?但你應該也發(fā)現(xiàn)了,這個Context.js可以說是一個通用的,在不同的場景,只需要實現(xiàn)ProviderClass中狀態(tài)管理這部分就行了,然后就稍微把Provider和Consumer這兩部分提取出來,寫個module,以后直接import直接用就好了,一直這么想,可這幾個月一直沒時間去實現(xiàn),每次都是yy / p拷貝過來直接用。其實復制粘貼也沒那么麻煩(ーー゛)。

? 最近終于有時間來總結一下了,這次實現(xiàn)了state的分離(直接寫一個普通的es6 class就行),以及多Provider的場景,而且Provider、Consumer的使用更靈活了,廢話不多說,直接來看一下最后的成果:

// 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中有些數(shù)據(jù)不想要被代理,也不想傳到Context中?
import { exclude } from 'ctx-react'

class Store{
    name: 'zz',
    @exclude temp: '這個字段不會進入到Context中'
}

? 這次真的沒了, 畢竟也就一百來行代碼,還要啥自行車。不過存在的一些潛在問題還是需要解決的,后續(xù)考慮加入scoop。

? 至于怎么實現(xiàn)的,其實大部分和上面對Context的封裝差不多,對于state的抽離這部分稍微要注意點,用到了es6的Proxy, 在監(jiān)聽到set時觸發(fā)更新,另外考慮到state中值為對象的情況,需要遞歸Proxy。

? 代碼已丟到github,https://github.com/evolify/ctx-react

? 也發(fā)到了npm:yarn add ctx-react

? 本以為最近能閑下來玩一下golang,這篇文章還沒寫完就又忙起來了,算了算了,還是先搬磚吧。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容