超性感的React Hooks(五):自定义hooks

简介: 在实践中,我们常常会遇到逻辑相同的功能片段。对于这样的场景,更省力的方式是,将这些功能片段封装成为一个单独函数来使用。例如,比较两个数组是否相等,可以封装一个equal方法,来处理这个通用逻辑。假设我们想要实现如下功能:比较左右两侧的数组是否相同。中间红色字为实时比较结果。每个数组都提供两个操作数组的按钮,点击一下,分别往原数组中添加数字1或者数字2 。

1


在实践中,我们常常会遇到逻辑相同的功能片段。对于这样的场景,更省力的方式是,将这些功能片段封装成为一个单独函数来使用。


例如,比较两个数组是否相等,可以封装一个equal方法,来处理这个通用逻辑。


export default function equalArr(a: number[], b: number[]) {
  if (a.length !== b.length) {
    return false;
  }
  if (a.length === 0 && b.length === 0) {
    return true;
  }
  return a.every((item, i) => item === b[i]);
}


假设我们想要实现如下功能:比较左右两侧的数组是否相同。中间红色字为实时比较结果。每个数组都提供两个操作数组的按钮,点击一下,分别往原数组中添加数字1或者数字2 。


微信图片_20220509204729.png


结合之前我们总结过的useState与刚才封装好的equalArr方法,能够简单实现我们想要的效果。


分别通过useState定义好两个数组。


const [arrA, setArrA] = useState<number[]>([]);
const [arrB, setArrB] = useState<number[]>([]);


因为每次state的变动,都会引起函数组件重新执行,因此,我们可以直接在useState之后添加如下代码,就能够在JSX中拿到最新的比较结果。


const [arrA, setArrA] = useState<number[]>([]);
const [arrB, setArrB] = useState<number[]>([]);
const isEqual = equalArr(arrA, arrB);


每个按钮,按要求执行对应的操作即可,例如点击一次,往数组中添加数字1


onClick={() => setArrA([...arrA, 1])}


实现后效果如图


微信图片_20220509204734.gif


完整代码如下:


import React, { useState } from 'react';
import {Button, Flex, Card} from 'antd-mobile';
import equalArr from './equalArr';
import './style.scss';
export default function EqualArr() {
  const [arrA, setArrA] = useState<number[]>([]);
  const [arrB, setArrB] = useState<number[]>([]);
  const isEqual = equalArr(arrA, arrB);
  return (
    <Flex className="queal_arr_container" justify="between" align="start">
      <Card className="inner left" title="左边数组arrA">
        <Button className="btn" onClick={() => setArrA([...arrA, 1])}>新增数字1到arrA</Button>
        {arrA.map((item, i) => (
          <div className="item" key={i}>{item}</div>
        ))}
        <Button className="btn" onClick={() => setArrA([...arrA, 2])}>新增数字2到arrA</Button>
      </Card>
      <Flex className="middle" justify="center" align="center">{isEqual.toString()}</Flex>
      <Card className="inner right" title="右边数组arrB">
        <Button className="btn" onClick={() => setArrB([...arrB, 1])}>新增数字1到arrB</Button>
        {arrB.map((item, i) => (
          <div className="item" key={i}>{item}</div>
        ))}
        <Button className="btn" onClick={() => setArrB([...arrB, 2])}>新增数字2到arrB</Button>
      </Card>
    </Flex>
  )
}


理解了这个例子,我们必须要思考的问题是,equalArr和普通的函数方法有什么不同吗?

按照以前的逻辑,我们会用怎么样的方式来来获得对比结果?


大概是这样,当我们点击时:


const clickCallback = () => {
  const newArrA = [...arrA, 1];
  const isEqual = equalArr(newArrA, arrB);
  // 各种处理比较结果isEqual的方式
  this.setState({ equal: isEqual });
}


唯一的区别在哪里?使用方式不一样!


老的思维,当我们点击时,


1.得到新的数组A,2.执行一次equalArr方法,得到比较结果,3.然后再处理结果。

而新的思维,当我们点击时,我们只关注数组A的变化!


数组变化之后会发生什么事情,全部都交给hook来处理。这些hook,可以是官方自定义的hook,如useEffect,也可以是我们自定义的hook,如此时的equalArr。


想想函数组件的一个特殊性:每次state改变,整个函数都会重新执行一次。那么基于这个特殊性,在新的思维里,我们可以乘机将equalArr也重新执行一次,只要我们能够确保传入的两个比较值为最新值,那么就能够在每次执行时得到最新的比较结果。


这是一次思维方式的减负。利用这样的特性,当触发点击事件时,我们就不再关注额外的逻辑,而只需要关注数组A的变化即可。


在React Hooks中,这样的自定义方法,我们就可以称之为自定义Hooks。


由于使用场景的特殊性,在自定义的hooks中,我们还可以使用所有官方提供的hooks,例如useState,useEffect等。因此我们所有的自定义hooks都会以use开头,以表示该方法只能在函数式组件中使用。


2


自定义hooks是对普通函数的一次增强。


上面的例子中,我们可以简单改造一下,重新自定义一个hook,useEqualArr


import { useState } from 'react';
function equalArr(a: number[], b: number[]) {
  if (a.length !== b.length) {
    return false;
  }
  if (a.length === 0 && b.length === 0) {
    return true;
  }
  return a.every((item, i) => item === b[i]);
}
export default function useEqualArr() {
  const [arrA, setArrA] = useState<number[]>([]);
  const [arrB, setArrB] = useState<number[]>([]);
  const isEqual = equalArr(arrA, arrB);
  return {
    arrA,
    setArrA,
    arrB,
    setArrB,
    isEqual
  }
}


在这个hook中,我们使用useState定义了两个数组,并将所有界面上需要的东西整合成一个对象返回。


那么使用时代码如下


/** 对比两个数组是否相等 */
import React from 'react';
import {Button, Flex, Card} from 'antd-mobile';
import useEqualArr from './useEqualArr';
import './style.scss';
export default function EqualArr() {
  const {arrA, arrB, setArrA, setArrB, isEqual} = useEqualArr();
  return (
    <Flex className="queal_arr_container" justify="between" align="start">
      <Card className="inner left" title="左边数组arrA">
        <Button className="btn" onClick={() => setArrA([...arrA, 1])}>新增数字1到arrA</Button>
        {arrA.map((item, i) => (
          <div className="item" key={i}>{item}</div>
        ))}
        <Button className="btn" onClick={() => setArrA([...arrA, 2])}>新增数字2到arrA</Button>
      </Card>
      <Flex className="middle" justify="center" align="center">{isEqual.toString()}</Flex>
      <Card className="inner right" title="右边数组arrB">
        <Button className="btn" onClick={() => setArrB([...arrB, 1])}>新增数字1到arrB</Button>
        {arrB.map((item, i) => (
          <div className="item" key={i}>{item}</div>
        ))}
        <Button className="btn" onClick={() => setArrB([...arrB, 2])}>新增数字2到arrB</Button>
      </Card>
    </Flex>
  )
}


是不是超级简单。


仅仅只是改变了写法,可是我们仔细分析一下,自定义hooks有自己特别的语法吗?其实没有。全都得益于state的改变,引发函数组件重新执行这一特性。


3


自定义hook能够跟随函数组件重复执行,并且每次都返回最新结果。因此,我们可以非常放心大胆的封装异步逻辑。


假设我们的项目中,有好几个的地方都要获取到最新的推送消息列表,那么我们就可以将这个逻辑封装成为一个hook。

image.gif

微信截图_20220509205139.png


如图,利用知乎日报提供的公共api来实现一个简单的列表获取功能。


首先创建api文件,定义数据请求的方式


import axios from 'axios';
interface Story {
  id?: number,
  ga_prefix?: string,
  hint?: string,
  image_hue?: string,
  title?: string,
  type?: number,
  url?: string,
  images?: string[]
  image?: string
}
export interface Feed {
  date: string,
  stories: Story[],
  top_stories: Story[]
}
export function zhLastFeedApi(): Promise<Feed> {
  return axios.get('https://news-at.zhihu.com/api/4/news/latest').then(res => {
    return res.data;
  });
}


其次自定义一个hook,该hook主要的目标就是通过请求上诉的api,获取到数据,并返回。


// useFeed
import {useState, useEffect} from 'react';
import {zhLastFeedApi, Feed} from './api';
export default function useFeed() {
  const [feed, setFeed] = useState<Feed>();
  useEffect(() => {
    zhLastFeedApi().then(res => {
      setFeed(res);
    })
  }, []);
  return feed;
}


公共的hook定义好之后,使用就非常简单了。抛开jsx除外,核心代码只有一句,给人的感觉就是直接执行了一个方法useFeed(),就得到了列表数据。


import React from 'react';
import useFeed from './useFeed';
import { ActivityIndicator, Carousel } from 'antd-mobile';
import './style.scss';
// 执行如下指令,禁用chrome跨域限制
// open -a "Google Chrome" --args --disable-web-security  --user-data-dir
export default function ZhihuFeed() {
  const feed = useFeed();
  if (!feed) {
    return <div className="feed_container loading"><ActivityIndicator /></div>
  }
  const {stories, top_stories} = feed;
  return (
    <div className="feed_container">
      <Carousel infinite autoplay dots={false}>
        {top_stories.map((item, i) => (
          <a className="top_feed_item" key={i} href={item.url}>
            <img src={item.image} alt="" />
            <div className="title">{item.title}</div>
          </a>
        ))}
      </Carousel>
      <div className="inner">
        {stories.map((item, i) => (
          <a className="feed_item" href={item.url} key={i}>
            <img src={item.images![0]} alt=""/>
            <div className="info">
              <div className="title">{item.title}</div>
              <div className="tip">{item.hint}</div>
            </div>
          </a>
        ))}
      </div>
    </div>
  )
}


这样,其他地方想要获取同样的列表,只需要执行一句话就可以了const feed = useFeed();


那么思考一个问题,如果此时我想要刷新怎么办?手动调用一次api吗?


微信图片_20220509204742.gif

当然不是。


还记得我们刚才说到的新的思维方式吗?当我们想要刷新时,我们只需要修改一个state状态值,让函数重新执行一次就可以了。根据此时的场景,引入一个loading状态,就可以简单达到我们的目的。


自定义hook useFeed修改如下


import {useState, useEffect} from 'react';
import {zhLastFeedApi, Feed} from './api';
export default function useFeed() {
  const [feed, setFeed] = useState<Feed>();
  const [loading, setLoading] = useState(true);
  useEffect(() => {
    // 做一个优化判断
    if (!loading) {
      return;
    }
    zhLastFeedApi().then(res => {
      setLoading(false);
      setFeed(res);
    })
  }, [loading]);
  return {feed, setLoading, loading};
}


然后使用时就能够通过自定义hook获取到最新的feed与loading。


import React from 'react';
import useFeed from './useFeed';
import { ActivityIndicator, Carousel, Button } from 'antd-mobile';
import './style.scss';
export default function ZhihuFeed() {
  const {feed, setLoading, loading} = useFeed();
  if (loading) {
    return <div className="feed_container loading"><ActivityIndicator /></div>
  }
  if (!feed) { return null };
  const {stories, top_stories} = feed;
  return (
    // .. 保持不变
    <Button onClick={() => setLoading(true)}>刷新</Button>
  )
}


点击刷新按钮时,我们只需要将loading设置为true即可。useEffect中监听到loading的变化,就会重新请求接口。因此,我们在点击事件的地方就不再去关注它请求数据的逻辑。


基于这样的思考,在实践项目中,我们大概率会重复实现方法去请求同样的数据,用户信息,某个配置项信息,权限信息等等,都可以使用这样的思路一次性解决。


4


再进一步思考。项目中,几乎每一个页面在初始化时都会请求一个接口。而关于这个接口就有许多共同的逻辑需要处理,例如请求成功返回数据,请求失败了页面响应失败信息,我们还需要处理刷新的逻辑。那么留下一个思考题,如何自己定义一个hook,解决这个场景?


// 如何实现?
export default function useInitial() {
}


5


认真体会上面所说的新的思维方式。


然后思维回拉到jQuery还横行世界的远古时期。我们想要实现一个超简单的小功能。点击一下按钮,元素div宽度增加10像素。


jQuery中,点击事件会关注那些内容?


1.在原始宽度基础上+10px2.给元素div设置新的宽度值


而React的点击事件呢?只关注一件事儿,那就是数据!


1.this.setState({ width: this.state.width + 10 })


只要我们能够正确处理好数据,React能够帮助我们将相应的页面元素改变渲染好。

而同样的道理,当逻辑变得复杂,我们即使只关注数据,也仍然会在处理数据时,额外思考很多其他的逻辑。


React hooks给我们提供的新思维是,我们只需要掌控一个开关,就能完成我们想要完成的事情。


因此,React与jQuery相比,是一次思维方式的革新与减负。React Hooks与之前的React相比,是另外一次思维革新与减负。这也是React Hooks简单并且高效的秘密。


6


最后留下一个思考题,我们常常会通过埋点,来精确计算一个页面的停留时长,那么如何利用自定义hooks的方式,来优雅的解决这个问题呢?


相关文章
|
22天前
|
前端开发 JavaScript API
探究 React Hooks:如何利用全新 API 优化组件逻辑复用与状态管理
本文深入探讨React Hooks的使用方法,通过全新API优化组件逻辑复用和状态管理,提升开发效率和代码可维护性。
|
24天前
|
前端开发
深入探索React Hooks:从useState到useEffect
深入探索React Hooks:从useState到useEffect
21 3
|
1月前
|
前端开发 JavaScript
深入探索React Hooks:从useState到useEffect
深入探索React Hooks:从useState到useEffect
|
1月前
|
前端开发 JavaScript 开发者
“揭秘React Hooks的神秘面纱:如何掌握这些改变游戏规则的超能力以打造无敌前端应用”
【10月更文挑战第25天】React Hooks 自 2018 年推出以来,已成为 React 功能组件的重要组成部分。本文全面解析了 React Hooks 的核心概念,包括 `useState` 和 `useEffect` 的使用方法,并提供了最佳实践,如避免过度使用 Hooks、保持 Hooks 调用顺序一致、使用 `useReducer` 管理复杂状态逻辑、自定义 Hooks 封装复用逻辑等,帮助开发者更高效地使用 Hooks,构建健壮且易于维护的 React 应用。
34 2
|
19天前
|
前端开发 JavaScript
React Hooks 深入解析
React Hooks 深入解析
20 0
|
19天前
|
前端开发
React Hooks:从基础到进阶的深入理解
React Hooks:从基础到进阶的深入理解
25 0
|
21天前
|
缓存 前端开发 开发者
深入理解React Hooks,打造高效响应式UI
深入理解React Hooks,打造高效响应式UI
29 0
|
1月前
|
前端开发 JavaScript 开发者
颠覆传统:React框架如何引领前端开发的革命性变革
【10月更文挑战第32天】本文以问答形式探讨了React框架的特性和应用。React是一款由Facebook推出的JavaScript库,以其虚拟DOM机制和组件化设计,成为构建高性能单页面应用的理想选择。文章介绍了如何开始一个React项目、组件化思想的体现、性能优化方法、表单处理及路由实现等内容,帮助开发者更好地理解和使用React。
68 9
|
2月前
|
前端开发
深入解析React Hooks:构建高效且可维护的前端应用
本文将带你走进React Hooks的世界,探索这一革新特性如何改变我们构建React组件的方式。通过分析Hooks的核心概念、使用方法和最佳实践,文章旨在帮助你充分利用Hooks来提高开发效率,编写更简洁、更可维护的前端代码。我们将通过实际代码示例,深入了解useState、useEffect等常用Hooks的内部工作原理,并探讨如何自定义Hooks以复用逻辑。
|
18天前
|
监控 前端开发 数据可视化
3D架构图软件 iCraft Editor 正式发布 @icraft/player-react 前端组件, 轻松嵌入3D架构图到您的项目,实现数字孪生
@icraft/player-react 是 iCraft Editor 推出的 React 组件库,旨在简化3D数字孪生场景的前端集成。它支持零配置快速接入、自定义插件、丰富的事件和方法、动画控制及实时数据接入,帮助开发者轻松实现3D场景与React项目的无缝融合。
66 8
3D架构图软件 iCraft Editor 正式发布 @icraft/player-react 前端组件, 轻松嵌入3D架构图到您的项目,实现数字孪生