react-copy-write 基於新 Context 和 immer 的 React 狀態管理庫

react-copy-write 一個比較新的 React 狀態管理庫,經過 immer 實現了可變狀態接口和部分 reselect 記憶功能。可經過下圖快速瞭解該庫的總體架構:html

rcw

Context

新的 Context 給出多個 API 解決了深層傳遞數據的笨重。 其中 Provider 提供的數據在整個組件樹中可認爲是 「全局」 的,而 Consumer 則能夠在任一層自由的訂閱該 「全局」 數據,全部的 Consumer 會隨着 Provider 的數據改變而從新渲染。可是有時會出現沒必要要的從新渲染,其主要緣由是判斷 「全局」 數據是否改變的依據是 Object.isreact

class App extends React.Component {
  render() {
    return (
      <Provider value={{ something: 'something' }}> <Toolbar /> </Provider>
    )
  }
}
複製代碼

每次 App 組件渲染 Providervalue 都是新的對象。解決該類問題方法就是將 value 的數據提高。git

class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      value: { something: 'something' }
    }
  }

  render() {
    return (
      <Provider value={this.state.value}> <Toolbar /> </Provider>
    )
  }
}
複製代碼

還有一種狀況也會出現多餘的渲染,當 Consumer 只訂閱了部分 Provider 提供的數據,由於 setState 的緣由,每次都會獲得新的數據,此時即便數據的值沒有改變, Consumer 仍然會從新渲染,。github

const themes = {
  light: {
    foreground: '#ffffff',
    background: '#222222'
  },
  dark: {
    foreground: '#000000',
    background: '#eeeeee'
  }
}

const ThemeContext = React.createContext({
  theme: themes.dark,
  another: 'aaa'
})

function ThemeButton(props) {
  return <button onClick={props.changeTheme}>Change Theme</button>
}

class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      theme: themes.light,
      another: 'bbb'
    }

    this.toggleTheme = () => {
      this.setState(state => ({
        theme: state.theme === themes.dark ? themes.light : themes.dark
      }))
    }
  }

  render() {
    return (
      <ThemeContext.Provider value={this.state.another}>
        <ThemeButton changeTheme={this.toggleTheme} />
        <ThemeContext.Consumer>
          {state => {
            console.log('re-render')
            return <button>{state}</button>
          }}
        </ThemeContext.Consumer>
      </ThemeContext.Provider>
    )
  }
}

ReactDOM.render(<App />, document.getElementById('root'))
複製代碼

每次點擊 ThemeButton 時,this.state.another 數值並無改變,可是控制欄都會顯示 're-render' 。解決這種問題的方法是使用 shouldComponentUpdate 方法,但針對深層數據 共享結構 纔是最佳選擇,immutable.js 和 immer 都提供了共享結構的方案。typescript

immer

共享結構或結構化共享或結構共享化(structural sharing)保證在更新數據時重用未改變的數據引用,immutable.js 和 immer 庫都擁有這種特性。數組

import produce from 'immer'

const baseState = [
  {
    todo: 'Learn typescript',
    done: true
  },
  {
    todo: 'Try immer',
    done: false
  }
]

const nextState = produce(baseState, draftState => {
  draftState.push({ todo: 'Tweet about it' })
  draftState[1].done = true
})

expect(baseState.length).toBe(2)
expect(nextState.length).toBe(3)

expect(baseState[1].done).toBe(false)
expect(nextState[1].done).toBe(true)

expect(nextState[0]).toBe(baseState[0]) // 未改變部分
expect(nextState[1]).not.toBe(baseState[1])
複製代碼

將上述兩部分連接在一塊兒,就是 react-copy-write 的目的。架構

react-copy-write

查看 react-copy-write 源碼僅有不到 200 行(註釋佔了一半),該庫對外提供了一個 API,接口返回一個對象ide

return {
  Provider: CopyOnWriteStoreProvider,
  Consumer: CopyOnWriteConsumer,
  Mutator: CopyOnWriteMutator,
  update,
  createMutator
}
複製代碼

Provider

Provider 做用與 Context.Provider 徹底一致。post

import createState from 'react-copy-write'

const State = createState({
  user: null,
  loggedIn: false
})

class App extends React.Component {
  render() {
    return (
      <State.Provider> <AppBody /> </State.Provider> ) } } 複製代碼

Consumer

Consumer 含有一個 selector 屬性,用來選取關注的數據this

const UserAvatar = ({ id }) => (
  <State.Consumer selector={state => state.users[id].avatar}>
    {avatar => (
      <div>
        <img src={avatar.src} />
      </div>
    )}
  </State.Consumer>
)
複製代碼

selector 屬性能夠是多個 selectors 組合,甚至是數組

// 多個 selectors 組合
const UserPosts = () => (
  <State.Consumer selector={state => ({ posts: state.posts, userId: state.user.id }}>
   {({posts, userId}) => {
     const filteredPosts = posts.filter(post => post.id === userId)
     return posts.map(post => <Post id={post.id} />)
   }
  </State.Consumer>
)

// selector 數組
const UserPosts = () => (
  <State.Consumer selector={[state => state.posts, state => state.userId]}>
    {[posts, userId] =>
        const filteredPosts = posts.filter(post => post.id === userId)
        return posts.map(post => <Post id={post.id} />)
    )}
  </State.Consumer>
)
複製代碼

在使用多個 selectors 時, selector 會依次執行,不過上述寫法會帶來沒必要要的渲染,由於每次都會生成新的對象。能夠修改成

const UserPosts = () => (
  <State.Consumer selector={state => state.posts}>
    {posts => (
      <State.Consumer selector={state => state.user.id}>
        {userId => {
          const filteredPosts = posts.filter(post => post.id === userId)
          return posts.map(post => <Post id={post.id} />)
        }}
      </State.Consumer>
    )}
  </State.Consumer>
)
複製代碼

可這種作法又過分防禦從新渲染,由於若是 state.posts 沒有改變,而 state.user.id 改變了,組件則沒有正常從新渲染。解決該問題的一個選擇即是使用數組方式。

update

ConsumerContext.Consumer 的區別在 children 屬性, Consumerchildren 會經過一箇中間組件,該中間組件不只進行了 shouldComponentUpdate 判斷,還將 update 賦予了 children

update 首先使用了 immer 的 produce 生成新的 state ,而後將其發送給 Provider

const Post = ({ id }) => (
  <div className="post">
    <State.Consumer selector={state => state.posts[id]}>
      // mutate 就是 update
      {(post, mutate) => (
        <>
          <h1>{post.title}</h1>
          <img src={post.image} />
          <p>{post.body}</p>
          <button
            onClick={() =>
              // Here's the magic:
              mutate(draft => {
                draft.posts[id].praiseCount += 1
              })
            }
          >
            Praise
          </button>
        </>
      )}
    </State.Consumer>
  </div>
)
複製代碼

回顧最初的圖,至此狀態管理造成了閉環:

  • update 經過 immer 生成共享結構的 state ,
  • Provider 更新 state ,
  • Consumer 經過 selector 訂閱部分 state, 並將 update 傳遞給 children 組件,
  • children 組件經過交互調用 update

我的拙見

  • 在 state 開銷可以承受的狀況下, Provider 徹底能夠做爲根組件存在。
  • 可否將 Provider 繼續包裝成 Observable ,聲明式更加方便(對了, createMutatorupdate 的聲明式語法糖),也能夠將 DOM 敏感和數據敏感結合。
  • 爲了模塊清晰調試簡單或實現時間旅行,是否可提供相似 combineReducers 的方法,將 Provider 組合。
相關文章
相關標籤/搜索