深度挖掘Concent的effect,全面提高useEffect的開發體驗

❤ star me if you like concent ^_^react

管理反作用代碼

在hook尚未誕生時,咱們一般都會在class內置的生命週期函數componentDidMountcomponentDidUpdatecomponentWillUnmount書寫反作用邏輯。git

這裏就再也不討論componentWillUpdatecomponentWillReceiveProps了,由於隨着react支持異步渲染後,這些功能已標記爲不安全,讓咱們跟隨者歷史的大潮流,完全忘記他們吧😀github

咱們來舉一個最典型的應用場景以下:編程

class SomePage extends Component{
    state = { products: [] }
    componentDidMount(){
        api.fetchProducts()
        .then(products=>this.setState({products}))
        .catch(err=> alert(err.message));
    }
}
複製代碼

這樣的相似代碼是你100%必定曾經寫過的,表達的含義也很簡單,組件初次掛載完畢時,獲取一下產品列表數據。api

咱們的頁面一般都會是這樣子的,頭部是一個條件輸入或者選擇區域,中央大塊區域是一個表格,如今咱們對這個頁面提一些需求,選擇區域裏任何值發生改變時,都觸發自動查詢更新列表,組件銷燬時作些其餘事情,親愛的讀者必定都寫過相似以下代碼:數組

class SomePage extends Component{
    state = { products: [], type:'', sex:'', addr:'', keyword:'' }
    
    componentDidMount(){
        this.fetchProducts();
    }
    
    fetchProducts = ()=>{
        const {type, sex, addr, keyword} = this.state;
        api.fetchProducts({type, sex, addr, keyword})
        .then(products=>this.setState({products}))
        .catch(err=> alert(err.message));
    }
    
    changeType = (e)=> this.setState({type:e.currentTarget.value})
    
    changeSex = (e)=> this.setState({sex:e.currentTarget.value})
    
    changeAddr = (e)=> this.setState({addr:e.currentTarget.value})
    
    changeKeyword = (e)=> this.setState({keyword:e.currentTarget.value})
    
    componentDidUpdate(prevProps, prevState){
        const curState = this.state;
        if(
            curState.type!==prevState.type ||
            curState.sex!==prevState.sex || 
            curState.addr!==prevState.addr || 
            curState.keyword!==prevState.keyword 
        ){
            this.fetchProducts();
        }
    }
    
    componentWillUnmount(){
        // 這裏搞清理事情
    }
    
    render(){
        const { type, sex, addr, keyword } = this.state;
        return (
            <div className="conditionArea">
                <select value={type} onChange={this.changeType} >{/**some options here*/}</select>
                <select value={sex} onChange={this.changeSex}>{/**some options here*/}</select>
                <input value={addr} onChange={this.changeAddr} />
                <input value={keyword} onChange={this.changeKeyword} />
            </div>
        );
    }
}
複製代碼

固然必定有騷氣蓬勃的少年不想寫那麼多change***,在渲染節點裏標記data-***來減小代碼,大機率以下:安全

class SomePage extends Component{
    changeKey = (e)=> this.setState({[e.currentTarget.dataset.key]:e.currentTarget.value})
    // 其餘略...
    render(){
        const { type, sex, addr, keyword } = this.state;
        return (
            <div className="conditionArea">
                <select data-key="type" value={type} onChange={this.changeKey} >
                    {/**some options here*/}
                </select>
                <select data-key="sex" value={sex} onChange={this.changeKey}>
                    {/**some options here*/}
                </select>
                <input data-key="addr" value={addr} onChange={this.changeKey} />
                <input data-key="keyword" value={keyword} onChange={this.changeKey} />
            </div>
        );
    }
}
複製代碼

若是此組件的某個狀態還須要接受來自props的值來更新,那麼使用class裏的新函數getDerivedStateFromProps替代了不推薦的componentWillReceiveProps,代碼書寫大體以下:bash

class SomePage extends Component{
    static getDerivedStateFromProps (props, state) {
        if (props.tag !== state.tag) return {tag: props.tag}
        return null
    }
}
複製代碼

到此,咱們完成了class組件對反作用代碼管理的討論,接下來咱們讓hook粉末登場━(`∀´)ノ亻!閉包

hook爸爸教作人

hook誕生之初,都拿上面相似例子來輪,會將上面例子改寫爲更簡單易懂的例子,分分鐘教class組件從新作人😀異步

咱們來看一個改寫後的代碼

const FnPage = React.memo(function({ tag:propTag }) {
  const [products, setProducts] = useState([]);
  const [type, setType] = useState("");
  const [sex, setSex] = useState("");
  const [addr, setAddr] = useState("");
  const [keyword, setKeyword] = useState("");
  const [tag, setTag] = useState(propTag);//使用來自props的tag做爲初始化值

  const fetchProducts = (type, sex, addr, keyword) =>
    api
      .fetchProducts({ type, sex, addr, keyword })
      .then(products => setProducts(products))
      .catch(err => alert(err.message));

  const changeType = e => setType(e.currentTarget.value);
  const changeSex = e => setSex(e.currentTarget.value);
  const changeAddr = e => setAddr(e.currentTarget.value);
  const changeKeyword = e => setKeyword(e.currentTarget.value);

  // 等價於上面類組件裏componentDidMount和componentDidUpdate裏的邏輯
  useEffect(() => {
    fetchProducts(type, sex, addr, keyword);
  }, [type, sex, addr, keyword]);
  // 填充了4個依賴項,初次渲染時觸發此反作用
  // 此後組件處於存在期,任何一個改變都會觸發此反作用
  
  useEffect(()=>{
      return ()=>{// 返回一個清理函數
          // 等價於componentWillUnmout, 這裏搞清理事情
      }
  }, []);//第二位參數傳空數組,次反作用只在初次渲染完畢後執行一次1
  
  useEffect(()=>{
     // 首次渲染時,此反作用仍是會執行的,在內部巧妙的再比較一次,避免一次多餘的ui更新
     // 等價於上面組件類裏getDerivedStateFromProps裏的邏輯
     if(tag !== propTag)setTag(tag);
  }, [propTag, tag]);

  return (
    <div className="conditionArea">
      <select value={type} onChange={changeType}>
        {/**some options here*/}
      </select>
      <select data-key="sex" value={sex} onChange={changeSex}>
        {/**some options here*/}
      </select>
      <input data-key="addr" value={addr} onChange={changeAddr} />
      <input data-key="tkeywordype" value={keyword} onChange={changeKeyword} />
    </div>
  );
});
複製代碼

看起來好清爽啊有木有,寫起來很騷氣似不似?巧妙的利用useEffect替換掉了類組件裏各個生命週期函數,並且上下文裏徹底沒有了迷惑的this,真面向函數式編程!

更讓人喜歡的是,hook是能夠自由組合、自由嵌套的,因此你的這個看起看起來很胖的FnPage裏的邏輯能夠瞬間瘦身爲

function useMyLogic(propTag){
    //剛纔那一堆邏輯能夠徹底拷貝到這裏,而後把狀態和方法返回出去
    return {
      type, sex, addr, keyword, tag,
      changeType,changeSex,changeAddr, changeKeyword,
    };
}

const FnPage = React.memo(function({ tag: propTag }) {
  const {
    type, sex, addr, keyword, tag,
    changeType,changeSex,changeAddr, changeKeyword,
   } = useMyLogic(propTag);
  // return your ui
});
複製代碼

useMyLogic函數能夠在其餘任意地方被複用!這將是多麼的方便,若是狀態更新比較複雜,官方還配套有useReducer來將業務邏輯從hook函數裏分離出去,以下代碼Dan Abramov給的例子:
點擊此處查看在線示例

const initialState = {
  count: 0,
  step: 1,
};

function reducer(state, action) {
  const { count, step } = state;
  if (action.type === 'tick') {
    return { count: count + step, step };
  } else if (action.type === 'step') {
    return { count, step: action.step };
  } else {
    throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  const { count, step } = state;

  useEffect(() => {
    const id = setInterval(() => {
      dispatch({ type: 'tick' });
    }, 1000);
    return () => clearInterval(id);
  }, [dispatch]);

  return (
    <>
      <h1>{count}</h1>
      <input value={step} onChange={e => {
        dispatch({
          type: 'step',
          step: Number(e.target.value)
        });
      }} />
    </>
  );
}
複製代碼

😀說到此處,是否是感受對class無愛了呢?可是這樣使用hook組織業務代碼真的就完美了嗎?沒有弱點了嗎?

使用Concent的effect,升級useEffect使用體驗

useMyLogic的確能夠處處複用,useReducer的確將狀態從hook函數再度解耦和分離出去,可是它們的問題以下:

  • 問題1,本質上來講,hook是鼓勵開發者使用閉包的,由於hook組件函數每一幀渲染建立了對應那一刻的scope,在scope內部生成的各類狀態或者方法都將只對那一幀有效,可是咱們逃不掉的是每一幀渲染都真真實實的建立了大量臨時的閉包函數,不短累計的確給js當即回收帶來了一些額外的壓力,咱們能不能避免掉反覆建立臨時閉包函數這些這個問題呢?答案是固然能夠,具體緣由參見往期文章setup帶來的變革,這裏主要主要討論useEffect和Concent的effect作對比,針對setup就不在次作贅述。
  • 問題2,useReducer只是解決了解耦更新狀態邏輯和hook函數的問題,可是它自己只是一個純函數,異步邏輯是沒法寫在裏面的,你的異步邏輯最終仍是落地到自定義hook函數內部,且useReducer只是一個局部的狀態管理,咱們能不能痛快的實現狀態更新可異步,可同步,能自由組合,且能夠輕易的提高爲全局狀態管理目的呢,答案是固然能夠,Concent的invoke接口將告訴你最終答案!
  • 問題3,useEffect的確解決了反作用代碼管理的詬病,可是咱們將類組件換爲函數組件時,須要代碼調整和邏輯轉換,咱們能不能統一反作用代碼管理方式,且讓類組件和函數組件能夠0改造共用呢,答案一樣是徹底能夠,基於Concent的effect接口,你能夠一行代碼不用改而實現統一的反作用管理,這意味着你的組件能夠任你在類與函數之間自由切換!

咱們總結一下將要解決的3個問題:

  • 1 避免反覆建立臨時閉包函數。
  • 2 狀態更新可異步,可同步,能自由組合,且能夠輕易的提高爲全局狀態管理目的。
  • 3 統一反作用代碼管理方式,讓類與函數實現0成本的無痛共享。

讓咱們開始表演吧

改造FnPage函數組件

構造setup函數

const setup = ctx => {
  const fetchProducts = (type, sex, addr, keyword) =>
    api
      .fetchProducts({ type, sex, addr, keyword })
      .then(products => ctx.setState({ products }))
      .catch(err => alert(err.message));

  ctx.effect(() => {
    fetchProducts();
  }, ["type", "sex", "addr", "keyword"]);//這裏只須要傳key名稱就能夠了
  /** 原函數組件內寫法: useEffect(() => { fetchProducts(type, sex, addr, keyword); }, [type, sex, addr, keyword]); */

  ctx.effect(() => {
    return () => {
      // 返回一個清理函數
      // 等價於componentWillUnmout, 這裏搞清理事情
    };
  }, []);
  /** 原函數組件內寫法: useEffect(()=>{ return ()=>{// 返回一個清理函數 // 等價於componentWillUnmout, 這裏搞清理事情 } }, []);//第二位參數傳空數組,次反作用只在初次渲染完畢後執行一次 */

  ctx.effectProps(() => {
    // 對props上的變動書寫反作用,注意這裏不一樣於ctx.effect,ctx.effect是針對state寫反作用
    const curTag = ctx.props.tag;
    if (curTag !== ctx.prevProps.tag) ctx.setState({ tag: curTag });
  }, ["tag"]);//這裏只須要傳key名稱就能夠了
  /** 原函數組件內寫法: useEffect(()=>{ // 首次渲染時,此反作用仍是會執行的,在內部巧妙的再比較一次,避免一次多餘的ui更新 // 等價於上面組件類裏getDerivedStateFromProps裏的邏輯 if(tag !== propTag)setTag(tag); }, [propTag, tag]); */

  return {// 返回結果收集在ctx.settings裏
    fetchProducts,
    //推薦使用此方式,把方法定義在settings裏,下面示例故意直接使用sync語法糖函數
    changeType: ctx.sync('type'),
  };
};
複製代碼

setup邏輯構造完畢了,咱們來看看函數組件是長什麼樣子滴

//定義狀態構造函數,傳遞給useConcent
const iState = () => ({ products:[], type: "", sex: "", addr: "", keyword: "", tag: "" });

const ConcentFnPage = React.memo(function({ tag: propTag }) {
  // useConcent返回ctx,這裏直接解構ctx,拿想用的對象或方法
  const { state, settings, sync } = useConcent({ setup, state: iState });
  const { products, type, sex, addr, keyword, tag } = state;
  const { fetchProducts } = settings;

  // 下面UI中使用sync語法糖函數同步狀態,若是爲了最求極致的性能
  // 可將它們定義在setup返回結果裏,這樣不用每次渲染都生成臨時的更新函數
  return (
    <div className="conditionArea">
      <h1>concent setup compnent</h1>
      <select value={type} onChange={sync('type')}>
        <option value="1">1</option>
        <option value="2">2</option>
      </select>
      <select data-key="sex" value={sex} onChange={sync('sex')}>
        <option value="1">male</option>
        <option value="0">female</option>
      </select>
      <input data-key="addr" value={addr} onChange={sync('addr')} />
      <input data-key="keyword" value={keyword} onChange={sync('keyword')} />
      <button onClick={fetchProducts}>refresh</button>
      {products.map((v, idx)=><div key={idx}>name:{v.name} author:{v.author}</div>)}
    </div>
  );
});
複製代碼

setup的強大之處在於,它只會在組件首次渲染以前執行一次,返回的結果蒐集在settings裏,這意味着你的api都是靜態聲明好的,而不是每次渲染再建立!同時在這個空間內你還能夠定義其餘的函數,如ctx.on定義事件監聽,ctx.computed定義計算函數,ctx.watch定義觀察函數等,這裏咱們重點講得是ctx.effect,其餘的使用方法能夠查閱如下例子:
codesandbox.io/s/concent-g…
stackblitz.com/edit/concen…

咱們如今看看效果吧

避免反覆建立臨時閉包函數

到此爲止,咱們解決了第一個問題即避免反覆建立臨時閉包函數

那若是咱們的狀態更新邏輯伴隨着不少複雜的操做,避免不了的咱們的setup body會越來臃腫,咱們固然能夠在把這些函數封裝一遍抽象出去,最後返回結果真後調用ctx.state去更新,可是concent提供更優雅的接口invoke讓你作這個事情,咱們將這些邏輯封裝成一個個函數放置在一個文件logic.js中,而後返回新的片斷狀態,使用invoke調用它們

//code in logic.js

export function simpleUpdateType(type, moduleState, actionCtx){
    return { type };
}
複製代碼

在你的setup體內你就能夠構造一個將被收集到settings裏的屬性調用該函數了。

import * as lc from './logic';

const setup = ctx=>{
    //其餘略
    return {
        upateType: e=> ctx.invoke(lc.simpleUpdateType, e.currentTarget.value);
    }
}
複製代碼

這也許看起來沒什麼嘛,不就是一個調用嗎,來來,咱們換一個異步的寫法

//code in logic.js
export async function complexUpdate(type, moduleState, actionCtx){
    await api.updateType(type);
    return { type };
}

// code in setup
import * as lc from './logic';

const setup = ctx=>{
    //其餘略
    return {
        upateType: e=> ctx.invoke(lc.complexUpdate, e.currentTarget.value);
    }
}
複製代碼

是否是看起來舒服多了,更棒的是支持咱們來書寫多個函數而後自由組合,你們或許注意到函數參數列表除了第一位payload,還有第二位moduleState,第三位actionCtx,若調用方不屬於任何模塊則第二爲參數是一個無內容的對象{},什麼時候有值咱們後面再作分析,這裏咱們重點看第三位參數actionCtx,能夠用它來串聯其餘的函數,是否是特別方便呢?

//code in logic.js
export async function complexUpdateType(type, moduleState, actionCtx){
    await api.updateType(type);
    return { type };
}

export async function complexUpdateSex(sex, moduleState, actionCtx){
    await api.updateSex(sex);
    return { sex };
}

export async function updateTypeAndSex({type, sex}, moduleState, actionCtx){
    await actionCtx.invoke(complexUpdateType, type);
    await actionCtx.invoke(complexUpdateSex, sex);
}

// code in setup
import * as lc from './logic';

const setup = ctx=>{
    //其餘略
    return {
        upateType: e=> {
            // 爲了配合這個演示,咱們另開兩個key存type,sex^_^
            const {tmpType, tmpSex} = ctx.state;
            ctx.invoke(lc.updateTypeAndSex, {type:tmpType, sex:tmpSex}};
        }
    }
}
複製代碼

那若是這個狀態我想其餘組件共享改怎麼辦呢?咱們只須要先將狀態的配置在run函數裏(z注:使用concent是必定要在渲染根組件前先調用run函數的),竟然在使用useConcent的時候,標記模塊名就ok了

先配置好模塊

import { useConcent, run } from "concent";
import * as lc from './logic';

run({
    product:{
        //這裏複用剛纔的狀態生成函數
        state: iState(), 
        // 把剛纔的邏輯函數模塊當作reducer配置在此處
        // 固然這裏能夠不配置,不過推薦配上,方便調用處不須要再引入logic.js
        reducer: lc,
    }
});
複製代碼

接下來在組件里加上模塊標記吧,和ConcentFnPage對比,僅僅是將state屬性改成了module並設定爲product

const ConcentFnModulePage = React.memo(function({ tag: propTag }) {
  // useConcent返回ctx,這裏直接解構ctx,拿想用的對象或方法
  const { state, settings, sync } = useConcent({ setup, module:'product' });
  const { products, type, sex, addr, keyword, tag } = state;
  const { fetchProducts } = settings;
    
  //此處略,和ConcentFnPage 一毛同樣的代碼
  );
});
複製代碼

注意哦,原ConcentFnPage依然能正常運行,一行代碼也不用改,新的ConcentFnModulePage也只是在使用useConcent時,傳入了module值並去掉state,ctx.state將有所屬的模塊注入,其餘的代碼包括setup體內也是一行都沒有改,可是它們運行起來效果是不同的,ConcentFnPage是無模塊組件,它的實例們狀態是各自孤立的,例如實例1改變了狀態不會影響實例2,可是ConcentFnModulePage是註冊了product模塊的組件,這意味着它的任何一個實例修改了狀態都會被同步到其餘實例,狀態提高爲共享是如此輕鬆!僅僅標記了一個模塊記號。

來讓咱們看看效果吧!注意concent shared comp2個實例的狀態是同步的。

到此爲止,咱們解決了第二個問題即狀態更新可異步,可同步,能自由組合,且能夠輕易的提高爲全局狀態管理,且提高的過程是如此絲滑與愜意。

統一反作用代碼管理方式

那咱們還剩最後一個目標:統一反作用代碼管理方式,讓類與函數實現0成本的無痛共享。

這對於Concent更是垂手可得了,總而言之,concent在setup裏提供的effect會自動根據註冊的組件類型來作智能適配,對於類組件適配了它的各個生命週期函數即componentDidMountcomponentDidMountcomponentWillUnmount,對於函數組件適配了useEffect,因此切換成本同樣的是0代價!

改寫後的class組件以下,ctx從this獲取,註冊的參數交給register接口,注意哦,setup也是直接複用了的。

class ConcentFnModuleClass extends React.Component{
  render(){
    const { state, settings, sync } = this.ctx;
    const { products, type, sex, addr, keyword, tag } = state;
    const { fetchProducts, fetchByInfoke } = settings;
  
    //此處略,一毛同樣的代碼
  }
}

export default register({ setup, module:'product' })(ConcentFnModuleClass);
複製代碼

來看看效果吧!

shared comp 是函數組件,shared class comp是類組件。

結語

本文到此結束,我知道親愛的你必定有很多疑惑,或者想親自試一試,以上代碼片斷的在線示例在這裏,歡迎點擊查看,fork,並修改

固然了,還爲你準備有一個生產可用的標準代碼模板示例
js: codesandbox.io/s/concent-g…
ts: codesandbox.io/s/concent-g…

人到中年,生活不易,禿頭幾乎沒法阻止,碼字艱辛,concent求包養,看上的看官就來顆✨星星唄 ❤ star me if you like concent ^_^

咱們知道hook的誕生提高了react的開發體驗,那麼對於Concent來講呢,它作的遠比你想的更多,代碼的拆分與組合,邏輯的分離與複用,狀態的定義與共享,都能給你的開發體驗再度幸福提高double or more,由於Concent的slogan是一個可預測、0入侵、漸進式、高性能的加強型狀態管理方案

最後想想,做爲提供者的我,華髮雖然開始已經墜落,但若是以我一我的掉落的代價換來更多開發這可以保留住那一頭烏黑亮麗的濃密頭髮,瞬間以爲值了,哈哈😀

相關文章
相關標籤/搜索