2twdtl公司

到目前为止,读者们已经了解了React 16.8最受欢迎的特性:hooks,你们肯定已经浏览过,浏览过,或者至少在书签上加了六篇文章。你可能读过或听说过他们有多伟大,有多可怕,甚至可能有多困惑。你可能在问你自己“我为什么要学这个?”你可能希望得到比这更好的答案“因为这是新事物”. 如果你按照几个钩子的指引走,你可能会发现自己在问“但是为什么呢?我可以用类做同样的事情!”

82BCA13D-8A45-4F10-822B-34D567B52973
信用卡@莉珊莫莉

如果这听起来很熟悉的话,那可能是因为这是我们每次面对学习新事物时所经历的同一个周期。学习新事物对任何人来说都是困难的,而重新学习一些你已经知道的东西会特别令人沮丧。你的本能反应可能是用你已经知道的东西来框架新事物。当我第一次在OkCupid向我的团队分享关于钩子的知识时,我制作了一个映射组件生命周期方法到钩子替代方案的图表,这让我看起来有点像这个家伙:188bet金宝搏官网

tumblr\u o16n2klpx1ta3qyvo1\u 1280塔姆布尔
作者注:向OkCupid网络团队道歉,因为他们是我的试验品188bet金宝搏官网

事实证明,这可能是一个非常混乱的方式来学习钩子!很多概念没有很好地映射,或者在一种方法和另一种方法中显得不必要的更复杂。我不会继续谈论我的失败,我会找到好的东西。这并不是一个全面的关于hooks的指南,但是我希望一旦你读完了,你会对用hooks编写你的第一个组件感兴趣。以我的经验来看,这才是真正的秘密:它不一定会点击,直到你开始为自己编写它们。毫无疑问,这是人类已知的学习钩子的最佳方法。

*我是说没关系
**我个人在出版时发现的


疲劳:设置状态

有线:使用状态

我们从基础开始。你可能已经看到了这个钩子的解释,如果是这样的话,请跳到下一节,在那里我们开始更深入地了解。

在React中,我们首先要做的事情之一就是生成一个有状态的组件。你和我一样,通过扩展反应组件(或者更可能的是,使用React.createClass类,但我们不谈论那些黑暗的日子)。你学会了使用此.setState({someKey:someValue})要修改组件的状态,请记住传入的键/值对设置状态用新值覆盖旧状态,其他所有内容都将被合并。哦,不要忘了初始化state对象,这样在尝试初始化时就不会出错设置状态. 当然,别忘了.绑定每一个将要修改构造函数中状态的函数,或者记住使用箭头函数语法几年前你的团队中有人安装了一个babel插件。

tumblr\ u inline\ u p2dyybPLrV1qgoj6i\ u 540系列

让我们暂时忘掉这一切。让我们概述一下我们需要构建什么,比如说,一个简单的、有状态的click counter组件:

  1. 我们需要知道当前的点击次数(我们称之为当前计数)
  2. 我们需要一种增加点击次数的方法(我们称之为设置当前计数)

如果我们想象一下我们有这些先决条件,我们可以这样写:

从“React”导入React;const Counter=()=>{return(
当前计数:{currentCount}
);};

通常我们会伸手去拿设置状态要将其变为现实,就意味着必须将这个微小的功能组件重构成一个完整的类组件。但坚持住,这是我们的第一个钩子。

导入React,{useState}来自“React”;const Counter=()=>{const[currentCount,setCurrentCount]=useState(0);return(
currentCount:{currentCount}
);};

“嗯,刚才发生了什么事?!”你可能在问自己。寒冷,自我。这是个钩子!挂钩允许功能组件钩入以前只对类组件可用的功能,例如状态。

这个使用状态hook是一个函数,它接受一个参数:初始状态(在本例中为0),并按此顺序返回数组中该状态值的值和setter。当您调用setter时,React使用更新的状态值重新呈现组件,就像您调用设置状态.

“为什么要破坏数组结构?”你问?好吧,这样你可以命名值和setter不管你喜欢什么。当然,你可以用使用状态在您的组件中,可以任意多次钩住,这样您就可以在需要时跟踪多个状态,而不必将状态表示转换为对象。在下一节中,我们将进一步了解这为我们提供的机会。

累:一个物体保持状态

有线:独立关注点的独立状态。

最有趣的事情之一使用状态组件的状态表示不存在作为一个对象,它可以是一个数字,一个字符串,或者任何你想要的东西(包括一个对象)。但这对于添加新的状态属性意味着什么呢?假设您稍后决定需要跟踪另一个有状态的属性。当state是一个对象时,这就像添加另一个键一样简单。现在,只需在使用状态:

从“React”导入React,{useState};const Counter=()=>{const[currentCount,setCurrentCount]=useState(0);const[isClicking,setIsClicking]=useState(false);return(
当前计数:{currentCount}正在点击:{isClicking}
);};

这也使我们能够灵活地将相关的代码块组合在一起,而不是将可能不相关的状态更改组合成一个设置状态打电话。例如,如果我想将一些事件处理程序移出返回块,我可以使用最合适的代码进行分组,如下所示:

从“React”导入React,{useState};const Counter=()=>{const[currentCount,setCurrentCount]=useState(0);const incrementCounter=()=>setCurrentCount(currentCount+1);const[isClicking,setIsClicking]=useState(false);const onMouseDown=()=>setIsClicking(true);const onMouseUp=()=>setIsClicking(false)return(
当前计数:{currentCount}正在单击:{isClicking}
);};

很酷,对吧?在传统的类组件中,这些状态属性必须同时存在于单个对象中,并且状态的初始化和修改它的函数可能分布在组件中,而不是与相关的逻辑组合在一起。对于这么简单的组件,好处可能不大,但是对于较大的组件,它确实可以对组件的可读性产生影响。

步骤:生命周期方法

有线:当你需要改变的时候改变的数据

所以我们学到了使用状态可以取代(并在某些方面改进)设置状态. 但是,我们可以在类组件中使用生命周期方法做的所有其他强大的事情呢?对于有经验的开发人员来说,这里的情况可能会变得有些棘手。

生命周期方法是一种抽象,它让我们思考呈现组件的阶段。名字像组件安装,组件更新,和组件将卸载对我们这些已经使用多年的人来说,感觉很直观,并且可靠地知道他们什么时候以及为什么会运行一些对初学者来说非常混乱的东西。也就是说,根据我的经验,我发现我们通常以一些可预测的模式使用它们。如果您对其中任何一个熟悉,请举手:

  1. 组件安装组件将卸载用于附加/删除事件侦听器,或设置/清除超时。(例如:侦听文档滚动或按键事件以更改状态)
  2. 组件安装组件更新根据道具/状态的变化来装载东西。(示例:在页面上着陆时加载数据,在状态更改时重新加载)
  3. 组件安装组件更新用于基于属性/状态更改重新计算某些DOM属性。(例如:状态更改后滚动到元素顶部)

通常,我们会同时使用其中的几种模式,哦,这些生命周期方法可能会变得混乱。相关的逻辑必然分布在这些方法中的一些方法中,并且很难理解kerfuffle中发生了什么。但不一定要这样!从根本上说,这些模式中的大多数都可以简化为:当某事发生时做某事。谢天谢地,我们有几个钩子可以帮上忙。

假设我们想去掉counter组件中的按钮,而只听文档上的单击。与其编写生命周期方法来设置和分解这些事件处理程序,不如使用一个名为使用效果.

从“React”导入React,{useState,useffect};const Counter=()=>{const[currentCount,setCurrentCount]=useState(0);const incrementCounter=()=>setCurrentCount(currentCount+1);useffect(()=>{文档.addEventListener(“click”,incrementCounter);return()=>{文档.removeEventListener(“单击”,递增计数器);};},[incrementCounter]);const[isClicking,setIsClicking]=useState(false);const onMouseDown=()=>setIsClicking(true);const onMouseUp=()=>setIsClicking(false);useffect(()=>{文档.addEventListener(“mousedown”,onMouseDown);文档.addEventListener(“mouseup”,onMouseUp);return()=>{文档.removeEventListener(“鼠标镇”,onMouseDown);文档.removeEventListener(“mouseup”,onMouseUp);};},[onMouseDown,onMouseUp]);return(
当前计数:{currentCount}正在单击:{isClicking}
);};

哇,太多了。让我们关注其中一个新的使用效果阻碍。

使用效果(()=>{文档.addEventListener(“click”,incrementCounter);return()=>{文档.removeEventListener(“click”,incrementCounter);};});

这个整洁的小钩子可能很难理解,使用好的老式命名函数可能更容易理解(我怀念那些):

useffect(函数setUp(){文档.addEventListener(“click”,incrementCounter);返回函数tearDown(){文档.removeEventListener(“click”,incrementCounter);};});

这样更好。本质上,我们告诉组件运行设置()函数,并使用拆卸函数,然后再进行下一次渲染。例如,如果此组件渲染三次,它将按如下方式运行函数:

  1. 提供
  2. 设置()
  3. 提供(x2个)
  4. 拆卸()
  5. 设置()
  6. 提供(3个)
  7. 拆卸()
  8. 设置()

……等等。但是,为了提高效率,我们可以选择通过使用效果第二个参数:

useffect(函数setUp(){文档.addEventListener(“click”,incrementCounter);返回函数tearDown(){文档.removeEventListener(“click”,incrementCounter);};},[增量计数器]);

第二个参数是应该导致组件重新运行设置拆卸功能。通常,此列表将包括您在使用效果在这种情况下打电话,递增计数器. 这让我们避免了浪费设置和撕裂。更强大的是,它可以帮助我们防止效果不止一次地运行,只需向它传递一个空的要更改的值列表。有帮助!

在上面的例子中,我们使用效果每次设置一些文档事件侦听器递增计数器但是我们可以使用这个钩子来运行任何我们想要的副作用,比如订阅和取消订阅一个web套接字,在一些属性改变时点击API获取更新的数据,或者我们可能想要采取的任何其他属性或状态驱动的操作。

值得注意的是,在这一点上拆卸返回值是完全可选的。我们并不总是有我们想清理的东西,但有时我们会清理,现在我们可以保持相关的逻辑在一起。它也可以是一个有用的提醒,以清理后,我们的副作用,我们可能已经忘记了清理在一个特定的时间组件将卸载.

方法:组件方法

有线:useCallback

“但是等等!”,你现在可能在说,“我们在呈现函数中定义了incrementCounter!每次渲染都会重新定义,所以我们的钩子不是每次都会运行吗?”好吧,我没有意识到你对这个过度拉伸的薄示例组件如此关注。但你绝对是对的!

从“React”导入React,{useState,useffect};const Counter=()=>{const[currentCount,setCurrentCount]=useState(0);const incrementCounter=()=>setCurrentCount(currentCount+1);useffect(()=>{文档.addEventListener(“click”,incrementCounter);return()=>{文档.removeEventListener(“单击”,递增计数器);};},[incrementCounter]);返回(
当前计数:{currentCount}
);};

因为递增计数器在每个渲染上都被重新定义,在的第二个参数中使用它使用效果不会给我们带来什么好处。谢天谢地,有一个钩子!

从“React”导入React,{useState,useffect,useCallback};const Counter=()=>{const[currentCount,setCurrentCount]=useState(0);const incrementCounter=useCallback(()=>setCurrentCount(currentCount+1),[setCurrentCount,currentCount],);useffect(()=>{文档.addEventListener(“单击”,递增计数器);return()=>{文档.removeEventListener(“click”,incrementCounter);};},[incrementCounter]);返回(
当前计数:{currentCount}
);};

这是我最喜欢的,也是最有用的钩子之一,即使你把钩子的整个概念都写下来,你也会想把这个放在你的工具带里。你给使用回调一个函数作为它的第一个参数,它返回它的一个记忆版本,只有在第二个参数中的任何项发生更改时才重新计算。

这是特别有用的,因为我们都知道传递箭头的功能就像道具一样不,布埃诺,因为这可能会导致浪费的重播。现在,修复它就像将箭头函数包装在使用回调钩子!这是一个简单的方法来挤出一些改进的性能,并防止不必要的重新渲染。

累:重新选择

有线:使用备忘录

如果我不说我的一个近亲,那我就是失职了使用回调,非常有用使用备忘录钩子。当您发现自己在渲染块中计算一些昂贵的东西时,请使用此钩子。例如,如果零部件实体的外观如下所示:

从“React”导入React;const MyComponent=({someObject})=>{const someNumber=对象.键(someObject).map((key)=>someObject[value]).filter((value)=>value%2===0).reduce((sum,current)=>sum+current,0)const array=[…new array(someNumber)];return(
{数组.map(()=>)}
);};

(当然,这是一个可笑的精心设计的例子,但是请直截了当地告诉我,你的代码库中没有这样的东西,我要把帽子吃掉了。)你可以把这个昂贵的计算包起来使用备忘录像这样:

导入React,{usemememo}来自“React”;const MyComponent=({someObject})=>{const array=usemememo(()=>{const someNumber=对象.键(someObject).map((key)=>someObject[value]).filter((value)=>value%2===0).reduce((sum,current)=>sum+current,0)return[…新数组(someNumber)];},[someObject]);return(
{数组.map(()=>)}
);};

现在,该数组将只有如果某物变化。这比在每次渲染时重新计算它要高效得多(尽管不可否认,仍然比完全删除它效率低,因为它非常糟糕)™️). 在过去,图书馆就像重新选择为我们提供了获得类似性能优势的工具,但现在您无需导入其他库即可获得这些优势。

疲劳:高阶组件/混合物

有线:自定义挂钩

还有一件事。回到我们的反例(你认为我永远不会让我们的计数器组件脱钩!),如果出于某种无法理解的原因,我们希望将单击处理程序附加到第二个组件中的文档,会怎么样?可能是一个在点击时产生随机数的。

从“React”导入React,{useState,useffect,useCallback};const randomnumergenerator=()=>{const[randomNumber,setRandomNumber]=useState();const getRandomNumber=useCallback(()=>setRandomNumber(4),//保证为随机[setRandomNumber],);useffect(()=>{文档.addEventListener(“点击”,获取随机数);return()=>{文档.removeEventListener(“click”,getRandomNumber);};},[getRandomNumber]);return(
随机数为:{randomNumber}
);};

好吧,我们可以重新定义新组件中的逻辑,但那不好玩。如果我们觉得自己很聪明,我们可以使用高阶组件或渲染道具来实现这一点。但这是定制钩子真正闪耀的地方。我们可以将共享逻辑抽象为一个定制的钩子,我们可以调用它使用文档单击.

从“React”导入React,{useState,useffect,useCallback};函数useDocumentClick(onDocumentClick){useffect(()=>{文档.addEventListener(“click”,onDocumentClick);return()=>{文档.removeEventListener(“click”,onDocumentClick);};},[onDocumentClick]);}常量计数器=()=>{const[currentCount,setCurrentCount]=useState(0);const incrementCounter=useCallback(()=>setCurrentCount(currentCount+1),[setCurrentCount,currentCount],);useDocumentClick(incrementCounter);return(
当前计数:{currentCount}
);};const RandomNumberGenerator=()=>{const[randomNumber,setRandomNumber]=useState();const getRandomNumber=useCallback(()=>setRandomNumber(4),//保证随机[setRandomNumber],);useDocumentClick(getRandomNumber);return(
random number is:{randomNumber}
);};

自定义钩子可以像其他钩子一样使用。自定义挂钩的名称应始终以开头使用,但除此之外,您还可以随意在其中执行任何操作,包括使用其他挂钩,如使用状态使用效果. 组件永远不需要知道钩子的实现细节,只需要知道它的API。这有助于将复杂的状态或副作用逻辑移出组件,并使该逻辑在将来易于重用。您不一定要对所有的钩子都这样做,但是当您需要时,这是一种更容易使组件逻辑可重用的方法。

例如,如果您发现自己经常从组件中进行API调用,那么可以编写一个名为使用API:

函数useAPI(method,endpoint,data){const[isLoading,setIsLoading]=useState(false);const[error,setError]=useState(null);const[response,setResponse]=useState(null);useffect(async()=>{try{setIsLoading(true);setResponse(null);setError(null);const res=await fetch(endpoint,{method,data});setIsLoading(false);setResponse(res);}catch(err){setIsLoading(false);setError(err);}},[方法,端点,数据]);return{response,error,isLoading,};}

这样,您就有了一个一致的层,用于从组件与API进行通信。如果你正在使用像GraphQL和阿波罗这样的现代技术,就像我们在OkCupid开始使用的那样,已经有一些了188bet金宝搏官网伟大的开源项目为您提供一些功能强大的自定义挂钩,以及不断增长的其他配套工具挂钩.

一句警告⚠️

在使用钩子的时候,有一个主要的规则你必须记住:你的钩子必须每次组件呈现时,都将以相同的顺序声明。意思是:钩子不能可以在条件内部、条件返回之后或循环中定义。钩子必须总是在缩进的“顶层”调用。如果这看起来很奇怪,那是因为根据Javascript标准,这是一个不寻常的限制。它是语言顶部的一个附加约束,但却是React以正确的方式保存状态所必需的约束。关于为什么存在这种限制的更多信息,我建议您阅读丹·阿布拉莫夫关于过度反应的博客文章. 好消息是:有一个埃斯林特插件帮助你避免犯那个错误。


你上钩了吗?

React钩子确实可以改变我们对组件中状态和状态更新的思考方式,这可以带来一些非常好的重构机会。虽然将我们的组件1:1从类“转换”为钩子很有诱惑力,但这通常会限制钩子所能提供的好处。我希望这本指南能够激发您对钩子功能的兴趣,同时也有助于重新定义我们对组件中一些常见模式的看法。使用使用回调使用备忘录怎么强调都不过分,因为它们可以在我们现有的功能组件中提供一些简单的性能优势。关于什么时候使用钩子和使用类组件更直观的争论肯定会很激烈,但我认为至少钩子在我们的工具带中为我们提供了一些非常强大的新工具,用于表达有状态的组件,甚至优化无状态的组件。