React Hooks 复习

React 函数组件中, 每一次 UI 的变化, 都是通过执行整个函数来完成的

函数组件: 当某个状态发生变化时, 我要做什么

Hooks 优点

  • 逻辑复用
  • 关注点分离

Hooks 使用规则

  • 只能在函数组件的顶级作用域使用
    • Hooks 不能在 循环 条件判断 嵌套函数内执行, 必须是顶层
    • 按顺序被执行
  • 只能在函数组件或其它 Hooks 中使用
    • 函数组件
    • 自定义 Hooks

useState 函数有维持状态的能力

state 永远不要保存可以通过计算得到值, 容易造成一致性问题

  • 从 props 传递过来的值
  • 从 URL 中读取的值
  • 从 cookie, localStorage 中读取的值
import React, { useState } from "react"

function Example() {
  // 创建一个保存 count 的 state,并给初始值 0
  const [count, setCount] = useState(0)

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  )
}

useEffect 执行副作用

useEffect 是每次组件 render 完后判断依赖并执行

// 第一个为要执行的函数 callback,第二个是可选的依赖项数组 dependencies
useEffect(callback, dependencies?)

// 没有依赖项
useEffect(() => {
  // 每次 render 完一定执行
  console.log('re-rendered');
});

// 空数组作为依赖项,则只在首次执行时触发
useEffect(() => {
  // 组件首次渲染时执行,等价于 class 组件中的 componentDidMount
  console.log('did mount');
}, [])

useCallback 缓存回调函数

function Counter() {
  const [count, setCount] = useState(0)
  // 在多次渲染之间,是无法重用 handleIncrement 这个函数的,而是每次都需要创建一个新的
  // 包含了 count 这个变量的闭包
  const handleIncrement = () => setCount(count + 1)
  // ...
  return <button onClick={handleIncrement}>+</button>
}
// fn 定义回调函数
// deps 依赖的变量数组, 某个依赖变化时, 才回重新声明 fn 这个回调
useCallback(fn, deps)
import React, { useState, useCallback } from "react"

function Counter() {
  const [count, setCount] = useState(0)
  // 只有当 count 发生变化时,我们才需要重新定一个回调函数
  const handleIncrement = useCallback(() => setCount(count + 1), [count])
  // ...
  return <button onClick={handleIncrement}>+</button>
}

useMemo 缓存计算结果

如果某个数据是通过其它数据计算得到的,那么只有当用到的数据,也就是依赖的数据发生变化的时候,才应该需要重新计算

  • 避免重新计算
  • 避免子组件重复渲染
// fn 产生所需数据的一个计算函数
// 通常来说,fn 会使用  deps 中声明的一些变量来生成一个结果,用来渲染出最终的 UI
useMemo(fn, deps)
//...
// 使用 userMemo 缓存计算的结果
const usersToShow = useMemo(() => {
    if (!users) return null;
    return users.data.filter((user) => {
      return user.first_name.includes(searchKey));
    }
  }, [users, searchKey]);

useCallback 的功能其实是可以用 useMemo 来实现的

const myEventHandler = useMemo(() => {
  // 返回一个函数作为缓存结果
  return () => {
    // 在这里进行事件处理
  }
}, [dep1, dep2])

useRef 在多次渲染之间共享数据

  • useRef 保存的数据一般是和 UI 的渲染无关的,因此当 ref 的值发生变化时,是不会触发组件的重新渲染的
  • 保存某个 DOM 节点的引用
    • 结合 React 的 ref 属性和 useRef 这个 Hook
const myRefContainer = useRef(initialValue)
import React, { useState, useCallback, useRef } from "react"

export default function Timer() {
  // 定义 time state 用于保存计时的累积时间
  const [time, setTime] = useState(0)

  // 定义 timer 这样一个容器用于在跨组件渲染之间保存一个变量
  const timer = useRef(null)

  // 开始计时的事件处理函数
  const handleStart = useCallback(() => {
    // 使用 current 属性设置 ref 的值
    timer.current = window.setInterval(() => {
      setTime((time) => time + 1)
    }, 100)
  }, [])

  // 暂停计时的事件处理函数
  const handlePause = useCallback(() => {
    // 使用 clearInterval 来停止计时
    window.clearInterval(timer.current)
    timer.current = null
  }, [])

  return (
    <div>
      {time / 10} seconds.
      <br />
      <button onClick={handleStart}>Start</button>
      <button onClick={handlePause}>Pause</button>
    </div>
  )
}
function TextInputWithFocusButton() {
  const inputEl = useRef(null)
  const onButtonClick = () => {
    // current 属性指向了真实的 input 这个 DOM 节点,从而可以调用 focus 方法
    inputEl.current.focus()
  }
  return (
    <>
      <input ref={inputEl} type='text' />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  )
}

useContext 定义全局状态

Context 提供了一个方便在多个组件之间共享数据的机制

// 1. 先创建一个上下文
const MyContext = React.createContext(initialValue)

// 2. Context.Provider 作为根组件
// 创建一个 Theme 的 Context
const ThemeContext = React.createContext(themes.light)
function App() {
  // 整个应用使用 ThemeContext.Provider 作为根组件
  return (
    // 使用 themes.dark 作为当前 Context
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar />
    </ThemeContext.Provider>
  )
}

// 3. 消费上下文
const value = useContext(MyContext)

Hooks 使用场景

自定义 Hooks: 声明一个名字以 use 开头的函数,比如 useCounter。这个函数在形式上和普通的 JavaScript 函数没有任何区别,你可以传递任意参数给这个 Hook,也可以返回任何值。

  • 名字一定以 use 开头的函数
  • 函数内部一定调用了其它的 Hooks, 可以是内置 Hooks, 也可以是其它自定义 Hooks.
  1. 封装通用逻辑: useAsync: 发起异步请求获取数据并显示在界面上
import { useState } from 'react';

const useAsync = (asyncFunction) => {
  // 设置三个异步逻辑相关的 state
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  // 定义一个 callback 用于执行异步逻辑
  const execute = useCallback(() => {
    // 请求开始时,设置 loading 为 true,清除已有数据和 error 状态
    setLoading(true);
    setData(null);
    setError(null);
    return asyncFunction()
      .then((response) => {
        // 请求成功时,将数据写进 state,设置 loading 为 false
        setData(response);
        setLoading(false);
      })
      .catch((error) => {
        // 请求失败时,设置 loading 为 false,并设置错误状态
        setError(error);
        setLoading(false);
      });
  }, [asyncFunction]);

  return { execute, loading, data, error };
};

export default function UserList() {
  // 通过 useAsync 这个函数,只需要提供异步逻辑的实现
  const {
    execute: fetchUsers,
    data: users,
    loading,
    error,
  } = useAsync(async () => {
    const res = await fetch("https://reqres.in/api/users/");
    const json = await res.json();
    return json.data;
  });

  return (
    // 根据状态渲染 UI...
  );
}
  1. 监听浏览器状态: useScroll
import { useState, useEffect } from "react"

// 获取横向,纵向滚动条位置
const getPosition = () => {
  return {
    x: document.body.scrollLeft,
    y: document.body.scrollTop,
  }
}
const useScroll = () => {
  // 定一个 position 这个 state 保存滚动条位置
  const [position, setPosition] = useState(getPosition())
  useEffect(() => {
    const handler = () => {
      setPosition(getPosition(document))
    }
    // 监听 scroll 事件,更新滚动条位置
    document.addEventListener("scroll", handler)
    return () => {
      // 组件销毁时,取消事件监听
      document.removeEventListener("scroll", handler)
    }
  }, [])
  return position
}

function ScrollTop() {
  const { y } = useScroll()

  const goTop = useCallback(() => {
    document.body.scrollTop = 0
  }, [])

  const style = {
    position: "fixed",
    right: "10px",
    bottom: "10px",
  }
  // 当滚动条位置纵向超过 300 时,显示返回顶部按钮
  if (y > 300) {
    return (
      <button onClick={goTop} style={style}>
        Back to Top
      </button>
    )
  }
  // 否则不 render 任何 UI
  return null
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容