深度挖掘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入侵、渐进式、高性能的加强型状态管理方案

最后想想,做为提供者的我,华发虽然开始已经坠落,但若是以我一我的掉落的代价换来更多开发这可以保留住那一头乌黑亮丽的浓密头发,瞬间以为值了,哈哈😀

相关文章
相关标签/搜索