14_React Hooks

React16.8之前Class组件的优势

  • class组件组件可以定义自己的state,
  • 有自己的生命周期如在componentDidMount中发送网络请求,并且该生命周期函数只会执行一次
  • 在状态改变之后只会重新执行render函数以及我们希望重新调用的生命周期函数componentDidUpdate等

而函数式组件不能定义自己的state,没有生命周期,在重新渲染时,整个函数都会执行。

Class组件存在的问题

  • 随着业务的增多,class组件会变得越来越复杂;各种逻辑混在一起,强行拆分反而会造成过度设计,增加代码的复杂度
  • ES6的Class API是学习React的一个障碍,前端开发人员必须搞清楚this指向问题,但是依然处理起来非常麻烦
  • 在复用组件状态时需要通过高阶组件,过多的高阶组件嵌套会导致代码变得难以理解

Hooks的出现

为了解决Class组件存在的问题,React16.8推出了Hook特性。它可以让我们在不编写class的情况下使用state以及其他的react特性(比如生命周期),可以由此延伸出非常多的用法,来让我们前面所提到的问题得到解决。

Hook的使用场景:

  • Hook的出现基本可以代替我们之前所有使用class组件的地方;
  • 但是如果是一个旧的项目,你并不需要直接将所有的代码重构为Hooks,因为它完全向下兼容,你可以渐进式的来使用它;
  • Hook只能在函数组件和自定义hook中使用,不能在类组件,或者函数组件之外的地方使用;

Hook是完全可选的 无需重写任何已有代码就可以在一些组件中尝试 Hook。

Hook是100% 向后兼容的 Hook 不包含任何破坏性改动。

Hook是现在可用 Hook 已发布于 v16.8.0。

案例对比

两个代码差异非常大,函数式组件结合hooks让整个代码变得非常简洁,并且再也不用考虑this相关的问题。

Hook使用注意事项

  • 只能在函数组件和自定义hook中使用,不能在类组件,或者函数组件之外的地方使用;
  • 只能在函数的最外层调用,不能在循环、条件判断、或者子函数中调用

useState

useState需要从react中导入,能够为函数组件提供状态(state)。

可以执行多次,每次执行互相独立,每调用一次为函数组件提供一个状态。

  • 参数:初始化值,只会在组件第一次渲染时生效 不设置则为undefined;
    • 如果初始state需要通过计算才能获得,则可以传入一个函数,在函数中计算并返回初始的state,此函数只在初始渲染时被调用
  • 返回值:数组,包含两个元素
    • 元素一:当前状态的值
    • 元素二:修改状态值的函数。设置新的状态后组件会重新渲染,根据新的值返回DOM结构。

注意事项

  1. useState的初始值(参数)只会在组件第一次渲染时生效 也就是说,以后的每次渲染,useState获取到都是最新的状态值,React组件会记住每次最新的状态值
  2. 修改状态的时候,一定要使用新得状态替换旧的状态,不能直接修改旧的状态,尤其是引用类型
javascript 复制代码
import { useState } from 'react'

function App({ message = '来自props' }) {
  // 参数:状态初始值比如,传入 0 表示该状态的初始值为 0
  // 返回值:数组,包含两个值:1 状态值(state) 2 修改该状态的函数(setState)
  const [count, setCount] = useState(0)
  const [message, setMessage] = useState(() => {
    return 'Hello Hooks' + props.message
  })

  function changeMessage() {
    setMessage('你好, Hooks')
  }
  
  return (
    <h2>Message: {message}<h2>
    <button onClick={changeMessage}>修改文本</button>
    <button onClick={() => { setCount(count + 1) }}>{count}</button>
  )
}
export default App

useReducer

useReducer和Redux并没有什么关系。

useReducer是useState的一种替代方案,在state的处理逻辑比较复杂时 或者 这次修改的state需要依赖之前的state时,可以使用useReducer。

const [state, dispatch] = useReducer(reducer, initialArg, init);

参数:

reducer : 对于我们当前state的所有操作都应该在该函数中定义,该函数的返回值,会成为state的新值

reducer在执行时,会收到两个参数:

state:当前最新的state

action:它需要一个对象,在对象中会存储dispatch所发送的指令

initialArg : state的初始值,作用和useState()中的值是一样

返回值:数组

第一个参数,state 用来获取state的值

第二个参数,state 修改的派发器,通过派发器可以发送操作state的命令,具体的修改行为将会由另外一个函数(reducer)执行

javascript 复制代码
import React, { memo, useReducer } from 'react'
// import { useState } from 'react'

// 为了避免reducer会重复创建,通常reducer会定义到组件的外部
function reducer(state, action) {
  switch(action.type) {
    case "add_number":
      return { ...state, counter: state.counter + action.num }
    case "sub_number":
      return { ...state, counter: state.counter - action.num }
    default:
      return state
  }
}

const App = memo(() => {
  // const [count, setCount] = useState(0)
  // const [counter, setCounter] = useState()
  // const [user, setUser] = useState()
  const [state, dispatch] = useReducer(reducer, { 
    counter: 0, 
    user: {
      name: 'jay'
    } 
  })

  return (
    <div>
      {/* <h2>当前计数: {count}</h2>
      <button onClick={e => setCount(count+1)}>+1</button>
      <button onClick={e => setCount(count-1)}>-1</button>*/}

      <h2>当前计数: {state.counter}</h2>
      <button onClick={e => dispatch({ type: "add_number", num: 5 })}>+5</button>
      <button onClick={e => dispatch({ type: "sub_number", num: 5 })}>-5</button>
    </div>
  )
})

export default App

useEffect

Effect Hook 可以让我们在函数式组件中完成一些类似于class组件中生命周期的功能。

什么是副作用(Side Effects)?副作用是相对于主做用来说的,一个函数除了主做用,其他作用就是副作用。

对于React组件来说,主做用就是根据数据(state/props)渲染UI,除此之外都是副作用(比如:手动修改dom)。

类似于网络请求、手动更新DOM、事件监听、localStorage操作等都是一些React更新DOM的副作用(Side Effects)。

useEffect解析:

  • 通过useEffect告诉React需要在渲染后执行某些操作
  • useEffect要求传入一个回调函数,在React执行完更新DOM操作之后,就会回调这个函数
  • 默认情况下,无论是第一次渲染之后,还是每次更新之后,都会执行这个回调函数
  • 可以按照代码用途多次使用useEffect,React按照effect声明的顺序依次调用组件中的每一个effect

基础使用

javascript 复制代码
import { useEffect, useState } from 'react'

function App() {
  const [count, setCount] = useState(0)
 
  useEffect(()=>{
    // dom操作
    document.title = `当前已点击了${count}次`
  })
  return (
    <button onClick={() => { setCount(count + 1) }}>{count}</button>
  )
}

export default App

依赖项控制执行时机

默认情况下,useEffect的回调函数每次渲染都会重新执行。某些情况下我们希望有些代码只执行一次即可,类似于在componentDisMount和componentWillUnmount完成的事情,另外多次执行也会导致一定的性能问题。

useEffect有两个参数,参数一是执行的回调函数,参数二决定useEffect在哪些state发生变化时,才会重新执行(依赖于谁)。

  • 不添加依赖项(默认): 组件首次渲染执行一次,以及不管是哪个状态更改引起组件更新时都会重新执行
  • 添加空数组: 组件只在首次渲染时执行一次
  • 添加特定依赖项: 副作用函数在首次渲染时和在依赖项发生变化时执行

注意事项:

useEffect 回调函数中用到的数据(比如,count)就是依赖数据,就应该出现在依赖项数组中,如果不添加依赖项就会有bug出现

javascript 复制代码
import { useEffect, useState } from 'react'

function App() {  
	const [count, setCount] = useState(0)  
	const [name, setName] = useState('chou') 

	useEffect(()=>{
		console.log('副作用执行了:没有依赖项')
	})

	useEffect(()=>{
		console.log('首次渲染时执行')
	}, [])

	useEffect(() => {    
		console.log('副作用执行了,count:', count)  
	}, [count])  

	return (    
		<div>      
			<button onClick={() => { setCount(count + 1) }}>{count}</button>      
			<button onClick={() => { setName('jay') }}>{name}</button>    
		</div>  
	)
}

export default App

清除副作用

在Class组件中,像定时器、一些订阅是需要在componentWillUnmount中取消的。

在useEffect中需要在回调函数中return一个新的函数,在新的函数中编写清理副作用的逻辑。

为什么需要返回一个新函数:清除机制是可选的,每个effect都可以返回一个清除函数方便讲添加和移除订阅的逻辑放在一起。

执行时机:组件更新时(下一个useEffect副作用函数执行之前)和卸载时自动执行。

javascript 复制代码
import { useEffect, useState } from "react"

const App = () => {
  const [count, setCount] = useState(0)
  
  useEffect(() => {
    const timer = setInterval(() => {
      setCount(count + 1)
    }, 1000)
    return () => {
      // 清除副作用
      clearInterval(timer)
    }
  }, [count])
  
  return (
    <div>
      {count}
    </div>
  )
}

export default App

useEffect - 发送网络请求

不可以直接在useEffect的回调函数外层直接包裹await,因为异步会导致清理函数无法立即返回

scss 复制代码
// 错误做法
useEffect(async ()=>{    
    const res = await fetch('http://localhost/getUserInfo')   
    console.log(res)
},[])

// 正确做法
useEffect(()=>{   
    async function fetchData(){      
       const res = await fetch('http://localhost/getUserInfo')              
    	 console.log(res)   
    } 
},[])

useContext

在之前的开发中,在组件中使用Context有两种方式:

  • 类组件可以通过 类名.contextType = MyContext 的方式,在类中获取context
  • 多个context或者在函数式组件中使用 MyContext.Consumer 组件回调函数的方式消费context

但是使用MyContext.Consumer消费多个Context时会存在大量的嵌套。

useContext将某个Context作为参数,获取Context的值。可以使用多次,获取多个Context的值。

javascript 复制代码
import { createContext, useContext } from 'react'

// 创建Context对象
const UserContext = createContext({name: 'jay'})
const ThemeContext = createContext({primaryColor: '#409eff'})

function Bar() {  
	// 底层组件通过useContext函数获取数据  
	const userCtx = useContext(UserContext)  
	const themeCtx = useContext(ThemeContext)  
	
	return (
		<div>
			<h2>Bar</h2>  
			<div>{userCtx.name}</div>>
		</div>
	)
}

function Foo() {  
	return (
		<div>
			<h2>Foo</h2> 
			<Bar/>
		</div>
	)
}

function App() {  
	return (    
		// 顶层组件通过Provider 提供数据    
		<Context.Provider value={'this is name'}>     
			<div>
				<Foo/>
			</div>    
		</Context.Provider>  
	)
}

export default App

useCallback

useCallback 允许你在多次渲染中缓存函数,在依赖不变的情况下,多次定义时,返回的函数是相同的。这种缓存返回值的方式叫做记忆化(memoization)。

constcachedFn = useCallback(fn,dependencies)

参数一:在多次渲染中需要缓存的函数

参数二:函数内部需要使用到的所有组件内部值的 依赖列表

useCallback只应该用于性能优化:

  • 跳过组件渲染
    • 普通函数在每次渲染时都会重新创建,如果传递给子组件,意味着子组件的props永远是不同的,这时memo对子组件性能的优化永远不会生效
    • 所以当需要将一个函数传递给子组件时,使用useCallback进行优化,将优化之后的函数,传递给子组件可以避免不必要的渲染
  • 优化自定义Hook: 使用useCallback包裹自定义hook中返回的函数,让使用者在需要时能够优化自己的代码
javascript 复制代码
import React, { memo, useCallback, useState } from 'react';

const A = memo((props) => {
  console.log('A渲染');
  const { onAdd } = props
  const [count, setCount] = useState(1);
  const clickAHandler = () => {
    setCount(prevState => prevState + 1);
  };

  return (
    <div>
      <h2>组件A -- {count}</h2>
      <button onClick={clickAHandler}>增加</button>
      <button onClick={onAdd}>增加App</button>
      {/* 100个组件 */}
    </div>
  )
})

let temp = null

const App = () => {
  console.log('App渲染');
  const [count, setCount] = useState(1);
  const [num, setNum] = useState(1);

  // 普通函数-每次都会重新创建,如果传递给子组件,意味着子组件的props永远是不同的,这是memo对性能的优化永远不会生效
  // const fun = () => {
  //   setCount(prevState => prevState + 1)
  // }
  // temp ? console.log(temp === fun) : temp = fun

  // 在依赖不变的情况下,多次定义时,返回的值是相同的
  const addCount = useCallback(() => {
    setCount(prevState => prevState + num);
  }, [count]);
  temp ? console.log(temp === addCount) : temp = addCount

  // 没有依赖项时,
  const addNum = useCallback(() => {
    // console.log('addNum')
    setNum(prevState => prevState + 1);
  }, []);

  return (
    <div>
      <h2>App Count-- {count}</h2>
      <h2>App Num-- {num}</h2>
      <button onClick={addCount}>增加Count</button>
      <button onClick={addNum}>增加Num</button>
      <div style={{ padding: '14px', border: '1px solid black' }}>
        <A onAdd={addNum} />
      </div>
    </div>
  )
}

export default App

useMemo

useMemo 在每次重新渲染的时候能够缓存计算的结果,多次定义时,返回的值是相同的。这种缓存返回值的方式叫做记忆化(memoization)

const cachedValue = useMemo(calculateValue, dependencies)

参数一:要缓存计算值的函数,它应该是一个可以返回任意类型且没有参数的纯函数。在首次渲染时调用该函数;在之后的渲染中,如果 dependencies 没有发生变化,React 将直接返回相同值,不再重新计算。否则,会再次调用 calculateValue函数计算最新结果并返回,然后缓存该结果以便下次重复使用。

参数二:所有在 calculateValue 函数中使用的响应式变量( props、state 和直接在组件中定义的变量和函数)组成的数组。

用于性能优化的点:

  • 跳过代价昂贵的计算:对于没有必要每次渲染都需要重新进行大量计算而得到的数据,应该使用useMemo对计算结果进行缓存,直到依赖项发生变化。
  • 跳过组件的重新渲染:对子组件传递相同内容的对象时,使用useMemo进行性能的优化
javascript 复制代码
import React, { memo, useCallback } from 'react'
import { useMemo, useState } from 'react'

const HelloWorld = memo(function (props) {
  console.log("HelloWorld被渲染")
  return <h2>Hello World</h2>
})

function calcNumTotal(num) {
  console.log("calcNumTotal的计算过程被调用")
  let total = 0
  for (let i = 1; i <= num; i++) {
    total += i
  }
  return total
}

const App = memo(() => {
  const [count, setCount] = useState(0)

  // 每次都会执行
  // const result = calcNumTotal(50)

  // 不依赖任何的值, 进行计算
  const result = useMemo(() => {
    return calcNumTotal(50)
  }, [])

  // 依赖count
  // const result = useMemo(() => {
  //   return calcNumTotal(count*2)
  // }, [count])

  // 使用useMemo对子组件渲染进行优化
  // const info = { name: "why", age: 18 }
  const info = useMemo(() => ({ name: "why", age: 18 }), [])

  // useMemo和useCallback的对比,
  // useMemo返回函数时和useCallback效果相同,但是更应该使用useCallback
  // function fn() { }
  // const increment = useCallback(fn, [])
  // const increment2 = useMemo(() => fn, [])
  // console.log(increment === increment2)

  return (
    <div>
      <h2>计算结果: {result}</h2>
      <h2>计数器: {count}</h2>
      <button onClick={e => setCount(count + 1)}>+1</button>

      <HelloWorld result={result} info={info} />
    </div>
  )
})

export default App

useRef

const ref = useRef(initialValue)

useRef 返回一个只包含一个current属性的ref对象,并且在后续的渲染中返回的 ref对象 是相同的。

它有两种用法:

  • 在函数组件中获取真实dom元素对象 或者 组件(class组件)实例
  • 使用ref对象的current属性保存数据,改变current属性不会触发重新渲染
javascript 复制代码
import { PureComponent, useEffect, useRef } from 'react'

class Home extends PureComponent {
  sayHi = () => {
    console.log('say hi')
  }
  render() {
    return <div>Home</div>
  }
}

function App() {
  // 获取dom和class组件的实例
  const h1Ref = useRef(null)
  const homeRef = useRef(null)

  useEffect(() => {
    console.log('h1Ref:', h1Ref) 
    console.log('homeRef', homeRef) 
    homeRef.current.sayHi()
  }, [])

  return (
    <div>
      <h1 ref={h1Ref}>this is h1</h1>
      <Home ref={homeRef}/>
    </div>
  )
}
export default App

useImperativeHandle

在使用forwardRef对 ref 进行转发时,子组件拿到父组件传递过来的ref,绑定到自己内部的元素上。这时父组件拿到的是完整的DOM对象,父组件可以进行任意的操作,这可能会导致某些不可控的情况。

useImperativeHandle允许你自定义暴露给父组件的对象,该对象包含你想暴露的方法。

useImperativeHandle(ref,createHandle,dependencies?)

参数 :

  • ref:该 ref 是你从 forwardRef 渲染函数 中获得的第二个参数。
  • createHandle:该函数无需参数,它返回你想要暴露的 ref 的句柄。该句柄可以包含任何类型。通常,你会返回一个包含你想暴露的方法的对象。
  • 可选的dependencies:函数 createHandle 代码中所用到的所有反应式的值的列表。反应式的值包含 props、状态和其他所有直接在你组件体内声明的变量和函数。
javascript 复制代码
import React, { memo, useRef, forwardRef, useImperativeHandle } from 'react'

const HelloWorld = memo(forwardRef((props, ref) => {
  const inputRef = useRef()

  // 子组件对父组件传入的ref进行处理
  useImperativeHandle(ref, () => {
    return {
      focus() {
        inputRef.current.focus()
      },
      setValue(value) {
        inputRef.current.value = value
      }
    }
  })

  return <input type="text" ref={inputRef} />
}))


const App = memo(() => {
  const titleRef = useRef()
  const inputRef = useRef()

  function handleDOM() {
    console.log(inputRef.current)
    inputRef.current.focus()
    inputRef.current.setValue("哈哈哈")
  }

  return (
    <div>
      <h2 ref={titleRef}>哈哈哈</h2>
      <HelloWorld ref={inputRef} />
      <button onClick={handleDOM}>DOM操作</button>
    </div>
  )
})

export default App

useLayoutEffect

useLayoutEffectuseEffect的一个版本。

  • useEffect是在浏览器重新绘制屏幕之后执行,不会阻塞DOM的是更新。
  • useLayoutEffect是在浏览器重新绘制屏幕之前执行,会阻塞DOM的是更新。
  • useLayoutEffect可能会影响性能,应该尽可能的使用useEffect
javascript 复制代码
import React, { memo, useEffect, useLayoutEffect, useState } from 'react'

const App = memo(() => {
  const [count, setCount] = useState(0)

  // 可以明显看到在useEffect中更改样式时出现了闪烁的现象
  // useEffect(() => {
  //   console.log("useEffect")
  //   if (count === 0) {
  //     setCount(Math.random() * 100)
  //   }
  // })

  // 在浏览器重新绘制屏幕之前更改样式不会出现闪烁
  useLayoutEffect(() => {
    console.log("useLayoutEffect")
    if (count === 0) {
      setCount(Math.random() * 100)
    }
  })

  console.log("App render")

  return (
    <div>
      <h2 style={{lineHeight: count + 'px'}}>count: {count}</h2>
      <button onClick={e => setCount(0)}>设置为0</button>
    </div>
  )
})

export default App

useDebugValue

为自定义 Hook 添加标签,标签会在 React 开发工具 中显示。

useDebugValue(value,format?)

useId

调用useId后生成一个唯一的ID,id不会因为组件的重新渲染而重新生成,适用于需要唯一id的场景,但不适用于列表的key。

const id = useId()

useId的主要作用是保证应用程序在服务端和客户端生成的ID是唯一的,从而避免通过其他手段生成的id不一致,而导致 hydration mismatch

javascript 复制代码
import React, { memo, useId, useState } from 'react'

const App = memo(() => {
  const [count, setCount] = useState(0)

  const id = useId()
  const id2 = useId()
  console.log(id, id2)

  return (
    <div>
      <label htmlFor={id}>
        用户名:<input id={id} type="text"/>
      </label>
      <h3>count:{count}</h3>
      <button onClick={e => setCount(count+1)}>count+1</button>
    </div>
  )
})

export default App

useTransition

useTransition可以在不阻塞UI的情况下更新状态,所以只有在可以访问该状态的set函数时才能使用。如果你想响应某个 prop 或自定义 Hook 值启动转换,请尝试使用 useDeferredValue.

const [isPending, startTransition] = useTransition()

useTransition返回一个pending状态和一个启动过渡任务的函数。传递给startTransition的回调函数必须是同步的。

在下面的例子中:

  • 将浏览器性能中的GPU进行6倍降速
javascript 复制代码
import React, { memo, useState, useTransition } from 'react'

const Home = () => {
  return (
    <h2>Home Page</h2>
  )
}

const Find = () => {
  console.log('Find')

  let lis = []
  for (let i = 0; i < 5000; i++) {
    lis.push(<li key={i} >{i}</li>)
  }

  return (
    <div>
      <h2>Find Page</h2>
      <ul>
        {lis}
      </ul>
    </div>
  )
}

const My = () => {
  return (
    <h2>My Page</h2>
  )
}

const TabBtn = (props) => {
  const { children, isActive, onClick } = props
  const [isPending, startTransition] = useTransition()

  if (isActive) {
    return <span style={{ color: '#409eff' }}>{children}</span>
  }

  if (isPending) {
    return <span style={{ color: 'gray' }}>{children}...</span>
  }

  return (
    <button
      onClick={() =>
        startTransition(() => {
          onClick()
        })
      }
      >
      {children}
    </button>
  )
}

const App = memo(() => {
  const [tab, setTab] = useState('Home')

  return (
    <div>
      <div>
        <TabBtn isActive={tab === 'Home'} onClick={() => setTab('Home')}>Home</TabBtn>
        <TabBtn isActive={tab === 'Find'} onClick={() => setTab('Find')}>Find</TabBtn>
        <TabBtn isActive={tab === 'My'} onClick={() => setTab('My')}>My</TabBtn>
      </div>
      {tab === 'Home' && <Home />}
      {tab === 'Find' && <Find />}
      {tab === 'My' && <My />}
    </div>
  )
})

export default App

useDeferredValue

const deferredValue = useDeferredValue(value)

useDeferredValue接收一个原始值(如字符串或者数字)或在渲染之外创建的对象(渲染期间创建的对象每次都不同,会导致不必要的重新渲染),返回该值的延迟版本。

在组件更新时会先尝试使用旧值 进行重新渲染,再在后台使用新值进行另一个重新渲染。这个后台渲染在 value 发生了变化时会中断,如果value变化的速度比重新渲染的速度快,那么组件只会在value不再变化时使用最新的值重新渲染,这个过程中会一直显示旧内容。

必须和React.memo一起使用才能有效。适用于状态的快速变化且访问不到该状态的set函数时,只对最后一次的渲染结果进行低优先级渲染。

在下面的例子中:

  • 将浏览器性能中的GPU进行6倍降速
  • 如果使用text传递到Item组件,在输入框连续输入123456789,Item组件中会打印 item 9000次,且输入框的内容会在渲染完后才显示。
  • 如果使用deferredText传递到Item组件,在输入框连续输入123456789,Item组件打印 item 的次数明显变少,且输入框的输入过程不会被阻塞。
    • Suspense的fallback效果只会在懒加载Item组件时出现。后续在新内容加载期间会显示旧内容。
    • 可以在新值和旧值不一样时,改变列表的样式,提示用户内容已过期。
    • useDeferredValue 引起的后台重新渲染在提交到屏幕之前不会触发 Effect。如果后台重新渲染被暂停,Effect 将在数据加载后和 UI 更新后运行。
javascript 复制代码
import { memo } from "react"

const Item = memo((props) => {
  console.log('item')
  const { text } = props

  useEffect(() => {
    console.log('useEffect')
  })

  return (
    <li>{text}</li>
  )
})

export default Item
javascript 复制代码
import React, { memo, useDeferredValue, useState, useTransition, Suspense } from 'react'
const Item  = React.lazy(() => import('./Item'))

const App = memo(() => {
  const [text, setText] = useState('')
  const deferredText = useDeferredValue(text)

  let items = []
  for (let i = 0; i < 1000; i++) {
    items.push(<Item key={i} text={deferredText} />)
  }

  return (
    <div>
      <input type="text" onInput={(e) => setText(e.target.value)} />
      <h2>列表: </h2>
      <ul style={{color: text !== deferredText ? 'gray' : 'black'}}>
        <Suspense fallback={<h2>...loading</h2>}>
          {items}
        </Suspense>
      </ul>
    </div>
  )
})

export default App

自定义Hook

自定义Hook其实只是一个普通的函数,严格意义上来说它并不算React的特性。

自定义Hook的名字需要使用 use 开头,如 useWindowScrolluseFetch

在自定义Hook中可以使用React自带的 Hook,方便我们对函数代码逻辑进行抽取。

javascript 复制代码
import { useState, useEffect, memo } from "react"

// lcoalStorage的存取
function useLocalStorage(key) {
  // 从localStorage中获取指定key的数据, 根据该数据创建组件的state
  const [data, setData] = useState(() => JSON.parse(localStorage.getItem(key)) || '')

  // data改变时将最近的值存储到localStorage
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(data))
  }, [data])

  // 将data和setData返回给组件, 让组件可以使用和修改值
  return [data, setData]
}

// 获取页面滚动位置
function useScrollPosition() {
  const [scrollX, setScrollX] = useState(0)
  const [scrollY, setScrollY] = useState(0)

  useEffect(() => {
    function handleScroll() {
      // console.log(window.scrollX, window.scrollY)
      setScrollX(window.scrollX)
      setScrollY(window.scrollY)
    }
    window.addEventListener("scroll", handleScroll)

    return () => {
      window.removeEventListener("scroll", handleScroll)
    }
  }, [])

  return [scrollX, scrollY]
}

// 生命周期
function useLifeCycleLog(cpnName) {
  useEffect(() => {
    console.log(cpnName + "组件被创建")
    return () => {
      console.log(cpnName + "组件被销毁")
    }
  }, [])
}

const Home = memo(() => {
  useLifeCycleLog('Home')

  return (
    <div>Home Page</div>
  )
})

const App = memo(() => {
  const [token, setToken] = useLocalStorage('token')
  const [scrollX, scrollY] = useScrollPosition()

  const [isShowHome, setIsShowHome] = useState(true)

  return (
    <div style={{ width: '2000px', height: '2000px' }}>
      <h1>App:</h1>
      <div>
        token: {token}
        <button onClick={() => setToken(Math.random())}>设置token</button>
      </div>
      <div>scrollX - {scrollX} , scrollY - {scrollY}</div>

      <div style={{padding: '20px', border: '1px solid green'}}>
        <button onClick={() => setIsShowHome(val => !val)}>切换</button>
        {isShowHome && <Home />}
      </div>
    </div>
  )
})

export default App
相关推荐
Endeavour_T3 分钟前
ECharts图表怎么做自适应?
前端·echarts
bo521003 分钟前
浏览器缓存优先级
前端·面试·浏览器
namehu4 分钟前
浏览器中的扫码枪:从需求到踩坑再到优雅解决
前端·react.js
opbr5 分钟前
🚫🔨 不用重构!教你用 Vue3 + TSX 🧹优雅收纳后台页面一堆操作按钮
前端·vue.js
杨进军9 分钟前
React 使用 MessageChannel 实现异步更新
react.js
G等你下课10 分钟前
使用 Cookie 实现登录登出功能案例
前端·后端
西瓜树枝15 分钟前
antd vue全局自定义样式前缀实践
前端·vue.js
前端进阶者16 分钟前
地图坐标系转换JS库
前端·javascript
蛙哇16 分钟前
Pinia 核心源码简易实现
前端
飞天牛牛16 分钟前
Shell 脚本里 nvm 不识别,node 却能用?原理与最佳实践
前端