concent 騷操做之組件建立&狀態更新

❤ star me if you like concent ^_^css

進化中的組件

隨着react 16.8發佈了穩定版本的hook特性,原來官網文檔裏對SFC的描述也修改成了FC,即無狀態函數組件變動爲了函數組件,官方代言人Dan Abramov也在各類場合開始向社區力推hook,將其解讀爲下一個5年React與時俱進的開端。html

仔細想一想,其實hook只是改變了咱們組織代碼的方式,由於hook的存在,咱們原來在類組件裏的各類套路均可以在函數組件裏找到一一對應的寫法,可是依託於class組件創建起來一系列最佳實踐在hook組件裏所有都要改寫,因此官方也是推薦如非必要,爲了穩妥起見老項目裏依然使用class組件react

任何新技術的出現必定都是有相關利益在驅動的,hook也不例外,官網對hook出現的動機給了3點重要解釋git

  • 在組件之間複用狀態邏輯很難
  • 複雜組件變得難以理解
  • 難以理解的 class

固然class組件最爲詬病的包裹地獄由於hook獨特的實現方式被消除了,因此class組件彷佛在將來的日子裏將慢慢被冷落掉,而hook本質只是一個個函數,對函數式編程將變得更加友好,同時還能繼續推動組合大於繼承的中心思想,讓更多的開發者受益於這種全新的開始思路並提高開發體驗。github

按照官方的願意表達,Hook既擁抱了函數,同時也沒有犧牲 React 的精神原則,提供了問題的解決方案,無需學習複雜的函數式或響應式編程技術。typescript

concent如何看待組件

前面有一句話提到「任何新技術的出現必定都是有相關利益在驅動的」,因此concent的誕生的動機也是很是明確:編程

  • 讓類組件和函數組件擁有徹底一致的編碼思路和使用體驗
  • 用最少的代碼表達狀態共享、邏輯複用等問題
  • 從組件層面搭建一個更優的最小化更新機制
  • 加強組件,賦予組件更多的強大特性

上面提到的第一點其實說白了統一類組件和函數組件,得益於concent能爲組件注入實例上下文的運行機制,不管是從api使用層面仍是渲染結果層面,都將高度給你一致的體驗,因此在concent眼裏,類與函數都是ui表達的載體而已,再也不區分對待它們,給用戶更多的選擇餘地。api

那麼廢話少說,咱們直接開整,看看concent提供了多少種建立組件很更新狀態的方式。數組

在展現和解讀組件建立和狀態更新代碼以前,咱們先使用run接口載入一個示例的業務model名爲demo,在如下代碼結構處於models文件夾。bash

這裏一個示例項目文件組織結構,不一樣的人可能有不一樣的理解方式和組織習慣,這裏只是以一個基本上社區上公認的通用結構做爲範原本爲後面的代碼解讀作基礎,實際的文件組件方式用戶能夠根據本身的狀況作調節

|____runConcent.js      # concent啓動腳本
|____App.css
|____index.js           # 項目的入口文件
|____models             # 業務models
| |____index.js
| |____demo             # [[demo模塊定義]]
| | |____reducer.js     # 更新狀態(可選)
| | |____index.js       # 負責導出demo model
| | |____computed.js    # 定義計算函數(可選)
| | |____init.js        # 定義異步的狀態初始化函數(可選)
| | |____state.js       # 定義初始狀態(必需)
| |____...
| 
|____components         # [[基礎組件]]
| |____layout           # 佈局組件
| |____bizsmart         # 業務邏輯組件(能夠含有本身的model)
| |____bizdumb          # 業務展現組件
| |____smart            # 平臺邏輯組件(能夠含有本身的model)
| |____pure             # 平臺展現組件
| 
|____assets             # 會被一塊兒打包的資源文件
|____pages              # 路由對應的頁面組件(能夠含有本身的model,即page model)
| |____...
| |
|____App.js
|____base
| |____config           # 配置
| |____constant         # 常量
|____services           # 業務相關服務
|____utils              # 通用工具函數

複製代碼

demo的state定義

export function getInitialState(){
    return {
        name: 'hello, concent',
        age: 19,
        visible: true,
        infos: [],
    }
}

export default getInitialState();
複製代碼

使用run接口載入模塊定義

// code in runConcent.js
import models from 'models';
import { run } from 'concent';

run(models);
複製代碼

對以上實例代碼有疑問能夠參考往期文章:
聊一聊狀態管理&Concent設計理念
使用concent,體驗一把漸進式地重構react應用之旅
或者直接查看官網文檔瞭解更多細節

建立類組件

使用register接口直接將一個普通類組件註冊爲concent類組件

import { register } from 'concent';
import React, { Component } from 'react';

@register('demo')
export default class ClassComp extends Component {
  render() {
    const { name, age, visible, infos } = this.state;
    return <div>...your ui</div>
  }
}
複製代碼

是的你沒看錯,這就完成了concent類組件的註冊,它屬於demo模塊,state裏將自動注入demo模塊的全部數據,讓咱們把它渲染出來,看看結果

function App() {
  return (
    <div>
      <ClassComp />
    </div>
  );
}

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

打開ReactDevTool查看dom結構

能夠看到頂層沒有任何Provider,數據直接打入組件內部,同時組件自己沒有任何包裹,只有一層,由於默認採用反向繼承的hoc策略,你的渲染的組件再也不產生大量Wrapper Hell...

或許有小夥伴會問這樣會不會打破了hoc模式的約定,由於你們都是使用屬性代理方式來作組件修飾,不破壞組件原有的任何結構,同時還能複用邏輯,但是這裏咱們須要多思考一下,若是邏輯複用不必定非要從屬性上穿透下來,而是直接能從實例上下文裏提供,那爲什麼咱們非要墨守成規的使用屬性代理的hoc模式呢?

固然concent對於類的修飾雖然默認使用了反向繼承,可是也容許用戶使用屬性代理,只須要開啓一個標記便可

@register({ module: 'demo', isPropsProxy: true })
export default class ClassComp extends Component{
  constructor(props, context){
    super(props, context);
    this.props.$$attach(this);// 屬性代理模式須要補上這句話,方便包裹層接管組件this
  }
  render(){
    const {name, age, visible, infos} = this.state;
    return <div>...your ui</div>
  }
}
複製代碼

顯而易見的,咱們發現已經多了一層包裹,之因此提供 isPropsProxy參數,是由於有些組件用到了多重裝飾器的用法,因此爲了避免破壞多重裝飾器下的使用方式而提供,但大多數時候,你都應該忘記這種用法,讓react dom樹保持乾淨清爽何樂而不爲呢?

圖中咱們看到組件名時$$CcClass1,這是一個當用戶沒有顯示指定組件名時,concent本身起的名字,大多數時候咱們能夠給一個與目標包裹組件同名的名字做爲concent組件的名字

//第二個可選參數是concent組件名
@register('demo', 'ClassComp')
export default class ClassComp extends Component{...}
複製代碼

建立CcFragment組件

CcFragment是concent提供的內置組件,可讓你不用定義和註冊組件,而是直接在視圖裏聲明一個組件實例來完成快速消費某個模塊數據的實例。

咱們在剛纔的App裏直接聲明一個視圖消費demo模塊的數據

function App() {
  return (
    <div> <ClassComp /> <CcFragment register="demo" render={ctx => { const { name, age, visible, infos } = ctx.state; return <div>...your ui</div> }} /> </div> ); } 複製代碼

渲染結果以下圖所示:

CcFragment採用的是 Render Props方式來書寫組件,特別適合一些臨時多個模塊數據的視圖片斷

<CcFragment register={{connect:['bar', 'baz']}} render={ctx => {
        // 該片斷鏈接了bar,baz兩個模塊,消費它們的數據
        const { bar, baz } = ctx.connectedState;
        return <div>...your ui</div>
      }} />
複製代碼

基於registerDumb建立組件

用戶一般在某些場合會基於CcFragment作經一步的封裝來知足一些高緯抽象的需求,concent自己也提供了一個接口registerDumb來建立組件,它本質上是CcFragment的淺封裝

const MyFragment = registerDumb('demo', 'MyFragment')(ctx=>{
  const { name, age, visible, infos } = ctx.state;
  return  <div>...I am MyFragment</div>
})
複製代碼

渲染結果以下圖所示:

能夠看到react dom tree上,出現了3層結構,最裏面層是無狀態組件實例。

基於hook建立組件

雖然registerDumb寫起來像函數組件了,但實際上出現了3層結構不是咱們但願看到的,咱們來使用hook方式重構此組件吧,concent提供了useConcent接口來建立組件,抹平類組件與函數組件之間的差別性。

function HookComp(){
  const ctx = useConcent('demo', 'HookComp');
  const { name, age, visible, infos } = ctx.state;
  return  <div>...I am HookComp</div>
}
複製代碼

渲染結果以下圖所示:

基於registerHookComp建立組件

registerHookComp本質上是useConcent的淺封裝,自動幫你使用React.memo包裹

const MemoHookComp = registerHookComp({
  module:'demo',
  render: ctx=>{
    const { name, age, visible, infos } = ctx.state;
    return  <div>...I am MemoHookComp</div>
  }
});
複製代碼

渲染結果圖裏咱們能夠看到tag上有一個Memo,那是React.memo包裹組件後DevTool的顯示結果。

concent如何看待狀態更新

上面的全部組件示例裏,咱們都只是完成的模塊狀態的獲取和展現,並無作任何更新操做,接下來咱們將對組件加入狀態更新操做行爲。

利用setState完成狀態更新

由於concent已接管了setState行爲,因此對於使用者來講,setState就能夠完成你想要的狀態更新與狀態同步。

在替換setState前,concent會保持一份引用reactSetState指向原始的setState,因此你大可沒必要擔憂setState會影響react的各類新特性諸如fiber 調度time slicing異步渲染等,由於concent只是利用接管setState後完成本身的狀態分發調度工做,自己是不會去破壞或者影響react自身的調度機制。

// 改寫ClassComp
@register('demo')
export default class ClassComp extends Component {
  changeName = (e)=> this.setState({name:e.currentTarget.value})
  render() {
    const { name } = this.state;
    return <input value={name} onChange={this.changeName} /> } } 複製代碼
// 改寫ClassComp
  <CcFragment register="demo" render={ctx => {
    const changeName = (e)=> ctx.setState({name:e.currentTarget.value});
    const { name, age, visible, infos } = ctx.state;
    return <input value={name} onChange={changeName} /> }} /> 複製代碼
// 改寫MyFragment
registerDumb('demo', 'MyFragment')(ctx=>{
  const changeName = (e)=> ctx.setState({name:e.currentTarget.value});
  const { name, age, visible, infos } = ctx.state;
  return <input value={name} onChange={changeName} /> }) 複製代碼
// 改寫HookComp
function HookComp(){
  const ctx = useConcent('demo', 'HookComp');
  const { name, age, visible, infos } = ctx.state;
  const changeName = (e)=> ctx.setState({name:e.currentTarget.value});
  return <input value={name} onChange={changeName} /> } 複製代碼
// 改寫MemoHookComp
const MemoHookComp = registerHookComp({
  module:'demo',
  render: ctx=>{
    const { name, age, visible, infos } = ctx.state;
    const changeName = (e)=> ctx.setState({name:e.currentTarget.value});
    return  <input value={name} onChange={changeName} /> } }); 複製代碼

能夠看到,因此的組件都是同樣的寫法,不一樣的是類組件還存在着一個this關鍵字,而在函數組件裏都交給ctx去操做了。

如今讓咱們經過gif圖演示看看實際效果吧

由於這些實例都是屬於demo模塊的組件,因此不管我修改任何一處,其餘地方視圖都會同步被更新,是否是將特別方便呢?

使用sync更新

固然若是對於這種單個key的更新,咱們也能夠不用寫setState,而是直接使用concent提供的工具函數sync來完成值的提取與更新

// 改寫HookComp使用sync來更新,其餘組件寫法都同樣,class組件經過this.ctx.sync來更新
function HookComp(){
  const ctx = useConcent('demo', 'HookComp');
  const {state: { name, age, visible, infos }, sync } = ctx.state;
  return <input value={name} onChange={sync('name')} /> } 複製代碼

使用dispatch更新

當咱們的業務邏輯複雜的時候,在真正更新以前要作不少數據的處理工做,這時咱們能夠將其抽到reducer

// 定義reducer,code in models/demo/reducer.js

export updateName(name, moduleState, actionCtx){
  return {name, loading: false};    
}

export updateNameComplex(name, moduleState, actionCtx){
   // concent會自動在reducer文件內生成一個名爲setState的reducer函數,免去用戶聲明一次
   await actionCtx.setState({loading:true});
   await api.updateName(name);
   // 在同一個reducer文件類的函數,能夠直接基於函數引用調用
   await actionCtx.dispatch(updateName, name);
}
複製代碼

在組件內部使用dispatch觸發更新

function HookComp(){
  const ctx = useConcent('demo', 'HookComp');
  const { name, age, visible, infos } = ctx.state;
  const updateNameComplex = (e)=>ctx.dispatch('updateNameComplex', e.currentTarget.value);
  return <input value={name} onChange={updateNameComplex} /> } 複製代碼

固然,這裏有更優的寫法,使用setup靜態的定義相關接口。瞭解更多關於setup

const setup = ctx=>{
  //這裏其實還能夠搞更多的事兒,諸如ctx.computed, ctx.watch, ctx.effect 等,下期再聊✧(≖ ◡ ≖✿)
  return {
    updateNameComplex: (e)=>ctx.dispatch('updateNameComplex',e.currentTarget.value),
  }
}
function HookComp(){
  // setup只會在組件初次渲染以前觸發一次!
  const ctx = useConcent({module:'demo', setup}, 'HookComp');
  const { name, age, visible, infos } = ctx.state;
  return <input value={name} onChange={ctx.settings.updateNameComplex} /> } 複製代碼

使用invoke更新

invoke給予用戶更自由的靈活程度來更新視圖數據,由於本質來講concent的reducer函數就是一個個片斷狀態生成函數,因此invoke讓用戶能夠不須要走dispatch套路來更新數據。

由於reducer定義是跟着model走的,爲了規範起見,實際編碼過程當中定義reducer函數比invoke更可以統一數據更新流程,很方便查看和排除bug。

function updateName(name, moduleState, actionCtx){
  return {name, loading: false};    
}

function updateNameComplex(name, moduleState, actionCtx){
   await actionCtx.setState({loading:true});
   await api.updateName(name);
   await actionCtx.invoke(updateName, name);
}

const setup = ctx=>{
  return {
    updateNameComplex: (e)=>ctx.invoke(updateNameComplex,e.currentTarget.value),
  }
}
function HookComp(){
  const ctx = useConcent({module:'demo', setup}, 'HookComp');
  const { name, age, visible, infos } = ctx.state;
  return <input value={name} onChange={ctx.settings.updateNameComplex} /> } 複製代碼

結語

經過以上示例,讀者應該能體會到統一類組件和函數組件的好處,那就是知足你任什麼時候段漸進式的書寫你的應用,不管是組件的定義方式和數據的修改方式,你均可以按需採起不一樣的策略,並且concent裏的hook使用方式是遵循着reducer承載核心業務邏輯,dispatch派發修改狀態的經典組織代碼方式的,可是並無強制約束你必定要怎麼寫,給予了你最大的自由度和靈活度,沉澱你我的的最佳實踐,甚至你能夠經過修改少許的代碼來100%複製社區裏現有的公認最佳實踐到你的concent應用。

(下2期預告:1 探究setup帶來的變革;2 concent love typescript,指望讀者多多支持,concent,衝鴨,to be the apple of your eyes)

❤ star me if you like concent ^_^

相關文章
相關標籤/搜索