ReactHook起飞指南作者:元小放僧雅集16.8发布了10个内置的hooks,但是基于以下两个API,你可以做很多事情。因此,本文不会讲很多API,也不会讲API的基本用法。它只会解释清楚可以做的这两件事。阅读全文大约需要5-10分钟。状态管理:useState副作用管理:useEffect这两个API就是钩子世界里的镰刀和锤子。这两个看似简单的API,实际上代表了一种与以往完全不同的全新编程模型。前言:已经有class组件了,怎么又多了一个hook?Dan在他的博客中提到:我们知道组件和自上而下的数据流可以帮助我们将大型UI组织成小的、独立的、可重用的部分。然而,我们通常不能进一步分解复杂的组件,因为逻辑是有状态的,不能提取到函数或其他组件中。钩子允许我们将组件内部的逻辑组织成可重用的独立单元。因此,用一句话来概括hooks带来的变化:将可复用的最小单元从组件层面进一步细化到逻辑层面。基于这个优势,后面我们可以看到基于hooks开发的应用中的所有组件都不会随着业务的增长而变得臃肿,因为在hooks的世界里,状态逻辑和UI是解耦的。UI只需要消费最终计算出来的状态数据,hook只关注状态的计算和变化。1.有状态函数useState组件是有状态的:classExampleextendsReact.Component{constructor(props){super(props);this.state={count:0};}render(){return(
你点击了{this.state.count}次
this.setState({count:this.state.count+1})}>点我);}}函数是无状态的:constExample=props=>{const{count,onClick}=props;return(
Youclicked{count}times
Clickme);}钩子是有状态函数:import{useState}from'反应';const示例=()=>{const[count,setCount]=useState(0);return(
你点击了{count}次
setCount(count+1)}>点击我{//data:{count:0,name:'zby'}->{count:0}setData({count:1});},[]);我们的应用是从小到大开发的,初期充分的组件划分和状态设计是保证应用后续可维护性的重要一环,因为随着应用的扩展,组件难免会变得臃肿,所以有时我们也会从一开始就一步一步引入状态管理,比如redux。但是现在,在我们的“纯函数”组件中,每个useState都会产生一对state和stateSetter,我们不需要考虑更多的状态树设计和组件划分设计,逻辑代码直接从根组件开始写,增量发展变得可行。所谓“增量”开发:一般应用的开发路径大致可以分为以下三个阶段:1.早期farm:只需要将相关状态组合成几个独立的状态变量即可应对大部分情况;2、Mid-termGank:当组件的状态逐渐增加时,我们可以轻松地将状态更新交给reducer进行管理(详见下文第2章展开);当逻辑越来越复杂时,我们可以将复杂的状态逻辑代码提取到自定义的hook中,以几乎零成本的方式解决问题(详见下文第3章展开);基于钩子,我们的开发过程变得比以往更有弹性。因此,这种增量开发变得可行且高效。2.高度灵活的redux,纯净无依赖正如上面所说,当组件的状态逐渐增加时,我们自然可以将状态更新交给reducer来管理。区别于真正的redux,在实际应用中,hook带来了更加灵活和纯粹的模式。现在我们可以用10行代码实现一个全局的redux,或者随时随地用2行代码实现一个本地的redux。A:10行代码实现一个全局的redux:importReactfrom'react';conststore=React.createContext(null);exportconstinitialState={name:'元宵'};exportfunctionreducer(state,action){switch(action.type){case'changeName':return{...state,name:action.payload};默认值:thrownewError('Unexpectedaction');}}导出默认存储;只需挂起Provider根组件importReact,{useReducer}from'react';importstore,{reducer,initialState}from'./store';functionApp(){const[state,dispatch]=useReducer(reducer,initialState);return(
)}子组件调用importReact,{useContext}from'react';从'./store'导入商店;functionChild(){const{state,dispatch}=useContext(store);...}B:随时随地实现部分reduximportReact,{useReducer}from'react';constinitialState={name:'元宵'};functionreducer(state,action){switch(action.type){case'changeName':return{...状态,名称:action.payload};默认值:thrownewError('Unexpectedaction');}}functionComponent(){const[state,dispatch]=useReducer(reducer,initialState);...}useState本质是useReducer的一个语法糖。有兴趣的可以去看看hooks的类型定义和实现。3、自定义hook上面说到,当组件发展到一定程度,不仅状态多了,状态的逻辑也越来越复杂,我们可以将复杂的状态逻辑代码提取到一个自定义的钩子中,以几乎零成本的方式解决问题。当我们想要在两个函数之间共享逻辑时,我们将其提取到第三个函数中。组件和钩子都是函数,所以这种方法也适用。不同的是,hook是一个有状态的函数,可以实现以往纯函数无法实现的更高层次的复用——状态逻辑的复用。然后先看下面两个演示示例A:classAppextendsReact.Component{constructor(props){super(props);this.state={count:0,name:undefined};}componentDidMount(){service.getInitialCount().then(data=>{this.setState({count:data});});service.getInitialName().then(data=>{this.setState({name:data});});}componentWillUnmount(){service.finishCounting().then(()=>{alert('计算完成');});}addCount=()=>{this.setState({count:this.state.count+1});};handleNameChange=name=>{this.setState({name});};render(){const{count,name}=this.state;return();}}B:函数useCount(initialValue){const[count,setCount]=useState(initialValue);useEffect(()=>{service.getInitialCount().then(data=>{setCount(data);});return()=>{service.finishCounting().then(()=>{alert('计算完成');});};},[]);函数addCount(){setCount(c=>c+1);}return{count,addCount};}functionuseName(initialValue){const[name,setName]=useState(initialValue);}useEffect(()=>{service.getInitialName().then(data=>{setName(data);});},[]);函数handleNameChange(value){setName(value);}return{name,handleNameChange};}constApp=()=>{const{count,addCount}=useCount(0);const{名称,setName}=useName();return();};A有2个State:count,name,相关逻辑分散在组件的生命周期和方法中。虽然我们可以将组件的状态和改变动作公开,但是涉及副作用的动作最终还是绕不开组件的生命周期。但是一个组件只有一套生命周期,难免会把一些完全不相关的逻辑写到一起。结果,无法实现完整的状态逻辑重用。在B中,我们通过自定义钩子将计数相关的逻辑和名称相关的逻辑封装在独立封闭的逻辑单元中。之前类组件的生命周期在这里已经不存在了。生命周期是一个与UI强耦合的概念。虽然很容易理解,但自然离数据很远。hook实现了类似rxjs模式的数据流订阅组件的sideeffect封装。这样做的好处是我们只需要关心数据。所以hook带来的不仅仅是简化state的定义和封装。这个动画很好的展示了A->B前后关联的状态逻辑的组织变化。商家实务记录:这是商品详情页使用的SKU选择组件。我们使用钩子来重构原来的类组件。这个重构过程是一个状态逻辑提取过程。我们将组件核心的两种状态和相关逻辑抽离成两个独立的自定义钩子(见下图)。这样做的好处是显而易见的,我们的组件只需要消费这两个钩子产生的值和函数,状态维护和更新的细节已经封装在钩子中了。const{specPath,handleSpecPathChange}=useSkuSpecPath({defaultSpecPath,dataSource});const{skus,handleSkuAmountChange}=useSkuAmount({defaultSelectedSkus,dataSource});下面是自定义钩子的设计范例:const{state,handleChange,others}=useCustomHook(config,dependency?);其中config声明了hook需要的数据,可能是内部useState的初始值,也可能是结构化数据。综上所述,这个hook的配置依赖一般只用在hook的API中,useEffect、useCallback等接口需要在我们声明依赖的时候传入。左边是重构后的代码(脱敏代码,只懂精神)。将原来的核心通用逻辑抽取出来放到useSkuSpecPath和useSkuAmount这两个hook中后,这个组件就成为了它们的调用者。不管未来UI组件如何变化和扩展,只要符合它们的界面格式约定,这些逻辑就可以随时随地复用,快速实现一个新的sku选择组件。总结:自定义hooks实现了状态逻辑和UI的分离,通过自定义hooks的合理抽象,可以实现非常高层的业务逻辑抽象复用。推荐一个收集一些有趣的自定义hooks的网站4.引用Dan未来的话:Hooks可以覆盖类组件的所有使用场景,同时在代码的抽取、测试和复用上提供更大的灵活性。这就是为什么Hooks代表了我们对React未来的愿景。在react16.8以上的应用中,可以直接使用。彩蛋钩子原理:不是魔术,只是arraylet钩子,i;functionuseState(){i++;if(hooks[i]){//再次渲染时返回hooks[i];}//首先渲染hooks.push(...);}//准备渲染i=-1;hooks=fiber.hooks||[];//调用组件Component();//缓存Hooks的状态fiber.hooks=hooks;