基于React Context Api 和 Es6 Proxy的状态管理

  近几个月的工作中,有遇到一些场景:基本不需要全局的状态管理,但页面级的,肯定需要在一些组件中共享,引入Redux这类状态管理库有点繁琐,直接通过props传递的话,写起来总觉得不是那么优雅。刚好项目中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>
  );
}
复制代码

  以上是官方文档中给出的用法,好处在于不用借助第三方状态管理库,也不需要手动传递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数组,如果不传,所有属性都会map
@Consumer(['list', 'query'])
export default class Page extends React.Component{
    render(){
        const { list, query } = this.props
        return(
            // ...
        )
    }
}
复制代码

  可以看到这里的Provider和Consumer很简洁,当然这也并非是Context中的Provider和Consumer,state状态的维护也抽离出去了,所有的这些逻辑是怎么实现的呢?先上代码:

// 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的逻辑,可以按照个人习惯来写。

  Provider的封装也比较简单,但同时也可以很灵活,可以在前面再加个参数,比如type之类的,然后使用的时候:@Provider(type),总之,按自己的需求来写。

  看起来Consumer的实现稍微复杂点,其实做的事情很简单,就是处理@Consumer()@Consumer('name')@Consumer(['key1', 'key2'])@Consumer(val=>({name: val.name}))这几种情况,毕竟想要更灵活嘛,而且,后面还实现了一种更灵活的Consumer.

  这么写好像更复杂了啊,比之前的代码还要多,还要难以理解?但你应该也发现了,这个Context.js可以说是一个通用的,在不同的场景,只需要实现ProviderClass中状态管理这部分就行了,然后就稍微把Provider和Consumer这两部分提取出来,写个module,以后直接import直接用就好了,一直这么想,可这几个月一直没时间去实现,每次都是yy / p拷贝过来直接用。其实复制粘贴也没那么麻烦(ーー゛)。

  最近终于有时间来总结一下了,这次实现了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中有些数据不想要被代理,也不想传到Context中?
import { exclude } from 'ctx-react'

class Store{
    name: 'zz',
  	@exclude temp: '这个字段不会进入到Context中'
}
复制代码

  这次真的没了, 毕竟也就一百来行代码,还要啥自行车。不过存在的一些潜在问题还是需要解决的,后续考虑加入scoop。

  至于怎么实现的,其实大部分和上面对Context的封装差不多,对于state的抽离这部分稍微要注意点,用到了es6的Proxy, 在监听到set时触发更新,另外考虑到state中值为对象的情况,需要递归Proxy。

  代码已丢到github,github.com/evolify/ctx…

  也发到了npm:yarn add ctx-react

  本以为最近能闲下来玩一下golang,这篇文章还没写完就又忙起来了,算了算了,还是先搬砖吧。

猜你喜欢

转载自juejin.im/post/5c4b257b51882525dd59391b
今日推荐