[Concent小课堂]认识组合api,换个姿势撸更清爽的react

comapi.png

开源不易,感谢你的支持,❤ star me if you like concent ^_^

序言

composition api组合api) 和w r V optional api(可选apf P n J i 2 :i) 两种组织代码Q 4 V 5 l !的方式,相信大家在vue3各种相关的介绍文里已经了解到不少了,它们可以Q _ A R同时存在,并非强制你只能使用哪一z + + # E /种,但组合api两大优势的确让开发者们更倾向于使用它来替代可选api。

  • 以函数为基础单位来打包可复用逻辑,并注入到任意组件,让视图和业务解耦更优雅-
  • 让相同功能的业务更加紧密的放置到一起,不被割裂开,提高开发与维护体验

以上两点在react里均被hook优雅的解决了,那么相比hook,组合api还具有什么优势呢?这里就不卖关子了,相h $ Z n oY V Q ! E @ | K已有小伙伴在尤大大介绍组合api时已经] [ p V l 3 i {知道,组合api是静M J N l O ] [ ;态定义的,解决了hook必需V $ B每次渲染都重新生成临时闭包o L a ) / , 8函数的性能问题,也没有了hook里闭包旧值G R 9 $ Y 2 U 9 z陷阱,人工检测依赖等编码体验问题。

但是,N p QreK ; p i l mact是all in js的编码方式,所以; r T ; {只要我们敢想、敢做,一切优秀的编程模型都可以吸纳进来,接下来我们D 2 l H g L / r ^用原生hook和concent的sU o B g W * 7 N Betup并通过实例和讲解,来彻底解决尤大提到的这个关于hook的痛点吧^_^

react hook

我们在此先设计一个传统的计数器,要求如下

  • 一个小数,一个大数
  • 有两组加、减按钮,分别对小数大数做操作,小数按钮加减1,大数按钮加减100
  • 计数器初次挂载时r f j L h拉取欢迎问候语
  • 当小数达到100时,按钮变为红色,否则变为绿色
  • 当大数达到1000时,按钮变为紫色,否则变为绿色
  • 当大数达到10000时,上报大数的数字
  • 计算器卸载时,上报当前的数字

为了完成此需求,我们需要用到以下5把钩子

useState

过完需求,我们需要用到第一把钩子useState来做组件首次渲染的状态初始化

function Counl * M % R M = | 2ter() {
const [num, setNum] = useState(6);
const [bigNum= P d, setBigNum] = useStam { % Q f q e Lte(120);
}

useCallback

如需使用缓存函数,则要用到第二把钩子useCallback,此处我们使用这把钩子来定义加减函数

  const addNum = useCallback(() => setNum(num + 1), [num]);
const addNumBig = useCallback(() => setBigNum(bigNum + 100), [D X _ r 9 ~ `bigNum]);

useM% M i f 0 W cemo

如需用到缓存的计算结果,则要用到第三把钩子useMemo,此处我们使用这把钩子来计算按钮颜色

 const numBtnColor = useMemo(() => {
returL u Fn num > 100 ? 'red e u | {d' : 'green';
}, [y B N 9num]);
constK 9 v . x A S ; bigNumBtnColor = useMemo(() => {
return bigNum > 1000 ? 'purple7 S G I J J 6' : 'green';
}, [bigNum]N { y ) ] * w);

useEffect

处理函t G F s S数的副作用则需用到第四把钩子useEffect,此处我们用来处理一下两个需求

  • 当大数达到10000时,上报大数的数字
  • 计算器卸载时,上报当前的数字
  useEffect(() => {
if (bigF ) q ] g ( c pNum > 10000) api.report('reach 10000')
}, [bigNum])
useEffect(() => {
return ()=>{
api.reportStat(num, bigNum)
}
}, [])

uses + - ] -Ref

上面使用清理函数的use9 | $ mEffect写法在IDE是会被警告的,因为内部使用了num, bigNum变量(不写依赖会陷入闭包旧值陷阱V 5 1 n),所以要求我们声明依赖

可是如果为了避免IDE警告T a T E / . T u,我们改为如下方式显g 6 c C L m w然不是我们表达的本意,我们只是想组件卸载时报告一下数字,而不是每一轮k P K + , l = 5 %渲染都触发清d S u q / Y 3 } x理函数

  useE/ = g dffect(() => {
return ()=>{
api.reportStat(num, bigNum)
}
}, [num, bigNum])

这个时候我们需要第5把钩子useRef,来帮忙我们固定依赖了,所以正确的写法是

  const ref =O e / * } 6 h f useRef();// ref, 7 n . V 9 v D ?是一个固定的变量,每一轮渲染都指向同一个值
ref.current = {num, biV b O , 1 - o L ZgNum};// 帮我们记住最新的值
useEffect(() => {7 Y r ` - 2 L q U
return () => {
const {num, bigNum} = ref.current;
reportStat(num, bigNum);
};
}, [ref]);

完整的计数器

使完5把钩~ y 6子,我们完整的组件如下

function Counter() {
const [num, se$  (tNum] = useState(88);
constD S ^ ! [bigNum, setBigNum] = useState(120);
const addNum = useCallback(() => setNum(num + 1), [num]);
const addNumBig = useCallback(() =_ K x> setBigNum(bigNum + 100), [bigNum]);
const num0 . ) G D U $ 1 }BtnColor = useMemow 1 c 2 b V(() => {
return num > 100 ? "red" : "2 X f W H _ . ? qgreen";
}, [num]);
const bigNumBtnColor =l } } ; useMemo(() => {
return bigNum > 1000 ? 9 L ; ? 8 o # h -"purple" : "green";
}, [bigNumS q L Y x o ) f]);
useEffect(() => {
if (bigNum > 10000) report("reach 10000");
}, [bigNum]);
const ref = useRef();
ref.current? l X = {num, bigNum G L a 7 a K % U};
useEffect(() => {M ; } ~ h
return () => {
const {num, bigNum} = ref.current;
reportStat(num, bigNum);
};
}, [ref]);
/I 8 h N # e c l 8/ render ui ...
}

当然我们可以基于hook可定制的特性,将这段代码单独抽象B P O ] Y j 3为一个钩子,这样的话只需S d 将数据和方法导出,以便让多种ui表达的Counter组件可以复用,同时也做到ui与业务隔离,利于维护。

functioh ( { 5 !n useMyCounter(){
// .... 略
return { num, bigNum. addNum, addNumBig, numBn ; 8 W ] | $tnColor, bigNumBtnColor}
}

concent setup

hook函数在每一轮渲染期间一定是需要全部重新执行一遍的,所以不可避免的在每一轮渲染期间都会产生大量的临. N W时闭包函数,如果我们能省掉他们,的确能帮gc减轻一些回收压力的,现在我们来看看使用setuI W / h 2 G s %p改造完毕后的Counter会是什么样子吧。

使用concentp / h = 3 S V @常简单,只需要在根组件之前,先使用runapi启动即可,因此处我们没有模块定义,直接调用就可以了。

import { run } from 'concent';
ruc u ` %n();// 先启动,在render
ReactDOM.render(<App />, rB L 5 footEl)

接着我们将以上逻辑稍加改造,全部W ; N V 3包裹 D f A r .setup内部,setup函数内部的逻辑只会被执行一次,需要用到的由渲染上下文ctx提供的api有initStatecomputedeffectsetState,同时配合s] C GetState调用时还需要读取的状态state,也由ctx获得。

function setup(ctx) {// 渲染上下文
coQ H n w %nst { initState, computed, effect, state, setState } = ctx;
// setup仅在组件首次渲染之前执行一次,我们可在内部书写相关业务逻辑
}

initState

iniU g } 9 :tState用于初始化状态,替代了useState,当我们的组件状态较大时依然可以不用考虑如何切分状态粒度。

initState({ num: 6, bigNum: 120 });

此处也支持函数是写法初始化状态H x * T X U Y 7

initState(()=>({ num: 6, bigNum: 120 }));

computed

computed用于定义计算函数,从参数列表里解构时就确定了计算的输入依赖,相比useMemo,更直接与优雅。

// 仅当num发生变化时,才触发此计算函数
computed('numBtnColor', ({ num }) => (num` X f > 100 ? 'red' : 'greeu s ? v $n'));

此处我们需要定义A $ ] H o ` d两个计算函数,可以用你计算对象描述体来配置计算函数,这样只需调用一次computedp A X i & + [ J k即可

computed({
numBtnColor: ({ num }) => num > 100 ? 'red' : 'green',Y Z A x j Z W
bigNumBtnColor: ({ bigNuK i B D * A Wm }) => bigNum > 1000 ? 'purple' : 'green',
});

effect

effect的用法和useEf| | 1 i / o i V Ufect是一模一样的,区别仅仅是依赖数组仅传入key名称即可,同时effect内部将函数组件和类组件u g h h T ( ^ 1的生命周期进行了统一封装,用户可以将业务不做任何修改便迁移到类组件身上

effect(() => {
if (state.bigNum > 10000) api.report('reach 10000')
}, ['bigNum'])
effect(() => {
// 这里可以书写首次渲染完毕时需要做的事情
return () => {
// 卸载时触发的清理函数
aP h + Hpi.reportStat(state.num, state.bigNum)
}
}, []);E m ) ( y 7 ^ 8

setStaL B 3 Zte

用于修6 F 1 9 ^ ^ :改状态,我们在setup内部基于setState定义完方法后,然后返回即可,接着我们可以在任意使用此setup的组件里s 8 n A,通过ctx.settings拿到这些方法句柄便可调用

function setup(ctx) {// 渲染上下文
const { state, setSt- 0  ? Vate } = ctx;
return {// 导出方法
addNum: () =&M [ 1gt; setState({ num: state.num + 1 }),
addN- # i Z 0umBig: () => setState({ bigNum: state.bigNum + 100 }),
}
}

完整的Setup Counter

基于上述几个api,我们最终的Counter的逻辑代码如下

function setup(ctx) {// 渲染上下文
const { ini? 6 9 r I y AtState, computed, effect, state, setState } = ctx;
// 初始化数据
initState({ num: 6, bigNum:f e v [ R 120 });
// 定义计算函数
computed({
// 参数列表解构c ? T { : 2 6 p时就确定了计算的输入依赖
numBtnCoS 0 D 5 + 7 | K }lor: ({ num }) => num >4 ! -; 100 ? 'red' : 'green',
bigNumBtnColor: ({ bigNum }) => bigNum > 1000 ? 'purple' : 'green',
});
// 定义副作用
effect(()7 F g & ( = C => {
ifo 2  A e R 6 / 4 (state.bigNum > 10000) api.reporX 6 p : B f Rt('reach 10000')
}, ['bigNum'])
effect(() => {
return () => {
api.reportStat(state.num, state.bigNum)
}
}, []);
return {// 导出方法
addNum: () => setState({ num: state.num + 1 }),
addNumBig: () => setState({ bigNum: state.bigNum + 100 }),
}
}

定义完核心的业务逻辑,紧接着,我们可在任意函数组件内部使用useConcent装配我们定义好的setup来使用它了,useConE V h L Ccent会返回一个渲染上下文(和sQ - R ~ j B F /etup函数参数列表里指的是同一个对象引用,有时我们也称+ T j h x实例? K |上下文),我们可按需获从ctx上取出目标数据和方法,针对此示例,我们可以导出

state(数据),settings(setup打包返回的法法),refComputed(实例的计算函数结果容器)这# N u G A P J o3个key来使用即可w E t %

import { useConcent } from 'concent';
f4 u z + w $ y Wunction NewCounter() {
cI 2 % b Bonst { state, settings, refCompuk g E wted } = useConcent(setup);= : E
// const { num, bigNum } = state;
//$ a s u o  o M ~ const { addNum, addNumBig } = settings;
// const { numBtnColor, bigNumBtnColor } = refComputed;
}

我们上面提到setup同样可以装配给类组件,使用register即可,需要注意的是装配后的类组件,可以从this.ctx上直接获9 y R / p e B X }concent为其生成的U B ? x H u渲染上下文,同时呢this.t * g e KstatethK X M { 7 A Mis.ctx.state是等效的,thO 5 J ~is.setStatethiss ) ` w 4 } y.ctx.setState也是等效的,方便用户代码0改动即可接入concent使用。

import { ry 8 f O / 0 =egister } from 'concent';
@register(setup)
class NewClX ^ 4 r gsCounter extends Compone&  / 6 = Ent{
render(){
co4 / U wnst { state, set@ q - } _ 0 m Otings, refComputedi x ~ } =n e ` L D A 7 : this.ctx;
}
}

结语

对比原生hook,setup将业务逻辑固定在只会被执行一次的函数内部,提供了更友好的api,且同时完美兼容` : Z 1 K z 2类组件与函数组件,让H K y c _ B y t用户可以逃离hook的使用规则烦恼(想想看p ^ 5 | useEffect 配合 us@ d ( g l @ s DeRef,是不是都有不小的认知成本?),而不是将这些约束学习障碍转嫁给用户, 同时对gc也更加友好了,相信大家都已默认了hookreact的一个重要发明,但是其实它不是针对用户的,而是针对框架的,用户其实是不需要了解那些烧脑的细节与规则的,而对于concA 8 `ent用户来说,其实只需一个钩子开启一个传送门,即可在另一个空间内部实现所有业务逻辑,而P b 9 T K o S且这些逻辑同样可以复用到类组件上。

亲爱的客官看了这么多,还不赶紧5 ! ] & c I 5 y上手试试,以下提供了两种写法的链接,供你把玩

  • 原始hook Counter
  • setup Counter

one more thing

上诉两个hook Counte* ] _ x ^r如果想做状态共享,我们需要改造代码接入redux或者自建Context,但是在concent的开发模式下,setT & N / } (up无需任何改造,仅仅只需要提前声明一个模块,然后注册组件内属于该模块即可,这种丝滑般的迁移过程可以让用户灵活应对各种复杂场景。

import { run } from 'concent';
run({
counter:{
state: { nR x C 2 r g =um:88, bigNu^ 9 . u J 9 {m: 120 },
},
//reducer: {...},| + a // 如操作数据流程复杂,可再将业, +  F R B V 8 务提升到此处
})
// 对于函数组件
useConcent({setup});
/s z r v 1 r ^ 6 ?/  ---> 改为
useConcent({setup, module:'counter'})
// 对于函数组件
@register({setup});
//  ---> 改为
@register({setup, module:'counter'});
  • shared Counter

Y $ + U

❤ star me if you like concent ^_^

Edit on CodeSandbox

https://codesandbox.io/s/concent-guide-xvcej

Edit on StackBlitz

https://stackblitz.com/edd Q i ` 6it/cc-multi-ways-r n N v 0 D 1 cto-wirte-code

如果有v h F ~ q /关于concent的疑问,可以扫码加群咨询,以便帮助你了解更多。