认识和体验Hooks
为什么需要Hook?
◼ Hook 是 React 16.8 的新增特性,它可以让我们在 不编写class的情况下 使用 state以及其他的React特性 (比如生命周期)。◼ 我们先来思考一下class组件相对于函数式组件有什么优势?比较常见的是下面的优势:◼ class组件可以 定义自己的state ,用来 保存组件自己内部的状态 ; 函数式组件不可以,因为函数每次调用都会产生新的临时变量;◼ class组件有 自己的生命周期 ,我们可以在 对应的生命周期中完成自己的逻辑 ; 比如在componentDidMount中发送网络请求,并且该生命周期函数只会执行一次; 函数式组件在学习hooks之前,如果在函数中发送网络请求,意味着每次重新渲染都会重新发送一次网络请求;◼ class组件可以 在状态改变时只会重新执行render函数 以及 我们希望重新调用的生命周期函数componentDidUpdate 等; 函数式组件在重新渲染时,整个函数都会被执行,似乎没有什么地方可以只让它们调用一次;◼ 所以,在Hook出现之前,对于上面这些情况我们通常都会编写class组件。
Class组件存在的问题
◼ 复杂组件变得难以理解: 我们在最初编写一个class组件时,往往逻辑比较简单,并不会非常复杂。但是 随着业务的增多 ,我们的 class组件会变得越来越复杂 ; 比如componentDidMount中,可能就会包含大量的逻辑代码:包括 网络请求、一些事件的监听 (还需要在componentWillUnmount中移除); 而对于这样的class实际上非常难以拆分:因为 它们的逻辑往往混在一起 , 强行拆分反而会造成过度设计 , 增加代码的复杂度 ;◼ 难以理解的class: 很多人 发现学习ES6的class是学习React的一个障碍 。 比如在class中,我们 必须搞清楚this的指向到底是谁 ,所以需要花很多的精力去学习this; 虽然我认为前端开发人员必须掌握this,但是依然处理起来非常麻烦;◼ 组件复用状态很难 : 在前面为了一些状态的复用我们需要 通过高阶组件 ; 像我们之前学习的 redux中connect或者react-router中的withRouter ,这些高阶组件设计的目的就是 为了状态的复用 ; 或者 类似于Provider、Consumer来共享一些状态 ,但是 多次使用Consumer时,我们的代码就会存在很多嵌套 ; 这些代码让我们 不管是编写和设计上来说,都变得非常困难 ;
Hook的出现
◼ Hook的出现,可以解决上面提到的这些问题;◼ 简单总结一下hooks: 它可以让我们在不编写class的情况下使用state以及其他的React特性 ; 但是我们 可以由此延伸出非常多的用法 , 来让我们前面所提到的问题得到解决 ;◼ Hook的使用场景: Hook的出现 基本可以代替我们之前所有使用class组件的地方 ; 但是如果是一个旧的项目,你 并不需要直接将所有的代码重构为Hooks ,因为 它完全向下兼容,你可以渐进式的来使用它 ; Hook 只能在函数组件中使用,不能在类组件 , 或者函数组件之外的地方使用 ;◼ 在我们继续之前,请记住 Hook 是: 完全可选的 : 你无需重写任何已有代码就可以在一些组件中尝试 Hook。但是如果你不想,你不必现在就去学习或使用 Hook。 100% 向后兼容的 : Hook 不包含任何破坏性改动。 现在可用 : Hook 已发布于 v16.8.0。
import React, { PureComponent } from 'react' class HelloWord extends PureComponent { constructor(props) { super(props) this.state = { message: "Hello World" } } changeText() { this.setState({ message: "你好啊, 李银河!" }) } render() { const { message } = this.state return ( <div> <h2>内容: {message}</h2> <button onClick={e => this.changeText()}>修改文本</button> </div> ) } } function HelloWorld2(props) { let message = "Hello World" // 函数式组件存在的最大的缺陷: // 1.组件不会被重新渲染: 修改message之后, 组件知道自己要重新进行渲染吗? // 2.如果页面重新渲染: 函数会被重新执行, 第二次重新执行时, 会重新给message赋值为hello world // 3.类似于生命周期的回调函数: 也是没有的 return ( <div> <h2>内容2: {message}</h2> <button onClick={e => message = "你好啊, 李银河!"}>修改文本</button> </div> ) } export class App extends PureComponent { render() { return ( <div> <HelloWord/> <hr /> <HelloWorld2/> </div> ) } } export default App
计数器案例对比
State/Effect
useState解析
认识useState
认识Effect Hook
需要清除Effect
import React, { memo, useEffect } from 'react' import { useState } from 'react' const App = memo(() => { const [count, setCount] = useState(0) // 负责告知react, 在执行完当前组件渲染之后要执行的副作用代码 useEffect(() => { // 1.监听事件 // const unubscribe = store.subscribe(() => { // }) // function foo() { // } // eventBus.on("why", foo) console.log("监听redux中数据变化, 监听eventBus中的why事件") // 返回值: 回调函数 => 组件被重新渲染或者组件卸载的时候执行 return () => { console.log("取消监听redux中数据变化, 取消监听eventBus中的why事件") } }) return ( <div> <button onClick={e => setCount(count+1)}>+1({count})</button> </div> ) }) export default App
使用多个Effect
◼ 使用Hook的其中一个目的就是解决class中生命周期经常将很多的逻辑放在一起的问题: 比如网络请求、事件监听、手动修改DOM,这些往往都会放在componentDidMount中;◼ 使用Effect Hook,我们可以将它们分离到不同的useEffect中: 代码不再给出◼ Hook 允许我们按照代码的用途分离它们, 而不是像生命周期函数那样: React 将按照 effect 声明的顺序依次调用组件中的 每一个 effect;Effect性能优化
import React, { memo, useEffect } from 'react' import { useState } from 'react' const App = memo(() => { const [count, setCount] = useState(0) // 负责告知react, 在执行完当前组件渲染之后要执行的副作用代码 useEffect(() => { // 1.修改document的title(1行) console.log("修改title") }) // 一个函数式组件中, 可以存在多个useEffect useEffect(() => { // 2.对redux中数据变化监听(10行) console.log("监听redux中的数据") return () => { // 取消redux中数据的监听 } }) useEffect(() => { // 3.监听eventBus中的why事件(15行) console.log("监听eventBus的why事件") return () => { // 取消eventBus中的why事件监听 } }) return ( <div> <button onClick={e => setCount(count+1)}>+1({count})</button> </div> ) }) export default App
执行时机 - 控制回调执行:
import React, { memo, useEffect } from 'react' import { useState } from 'react' const App = memo(() => { const [count, setCount] = useState(0) const [message, setMessage] = useState("Hello World") useEffect(() => { console.log("修改title:", count) }, [count]) useEffect(() => { console.log("监听redux中的数据") return () => {} }, []) useEffect(() => { console.log("监听eventBus的why事件") return () => {} }, []) useEffect(() => { console.log("发送网络请求, 从服务器获取数据") return () => { console.log("会在组件被卸载时, 才会执行一次") } }, []) return ( <div> <button onClick={e => setCount(count+1)}>+1({count})</button> <button onClick={e => setMessage("你好啊")}>修改message({message})</button> </div> ) }) export default App
Context/Reducer
useContext的使用
在组件中使用:import { createContext } from "react"; const UserContext = createContext() const ThemeContext = createContext() export { UserContext, ThemeContext }
import React, { memo, useContext } from 'react' import { UserContext, ThemeContext } from "./context" const App = memo(() => { // 使用Context const user = useContext(UserContext) const theme = useContext(ThemeContext) return ( <div> <h2>User: {user.name}-{user.level}</h2> <h2 style={{color: theme.color, fontSize: theme.size}}>Theme</h2> </div> ) }) export default App
useReducer
useState的替代方案
import React, { memo, useReducer } from 'react' // import { useState } from 'react' function reducer(state, action) { switch(action.type) { case "increment": return { ...state, counter: state.counter + 1 } case "decrement": return { ...state, counter: state.counter - 1 } case "add_number": return { ...state, counter: state.counter + action.num } case "sub_number": return { ...state, counter: state.counter - action.num } default: return state } } // useReducer+Context => redux const App = memo(() => { // const [count, setCount] = useState(0) const [state, dispatch] = useReducer(reducer, { counter: 0, friends: [], user: {} }) // const [counter, setCounter] = useState() // const [friends, setFriends] = useState() // const [user, setUser] = useState() return ( <div> {/* <h2>当前计数: {count}</h2> <button onClick={e => setCount(count+1)}>+1</button> <button onClick={e => setCount(count-1)}>-1</button> <button onClick={e => setCount(count+5)}>+5</button> <button onClick={e => setCount(count-5)}>-5</button> <button onClick={e => setCount(count+100)}>+100</button> */} <h2>当前计数: {state.counter}</h2> <button onClick={e => dispatch({type: "increment"})}>+1</button> <button onClick={e => dispatch({type: "decrement"})}>-1</button> <button onClick={e => dispatch({type: "add_number", num: 5})}>+5</button> <button onClick={e => dispatch({type: "sub_number", num: 5})}>-5</button> <button onClick={e => dispatch({type: "add_number", num: 100})}>+100</button> </div> ) }) export default App
Callback/Memo
useCallback
import React, { memo, useState, useCallback, useRef } from 'react' // useCallback性能优化的点: // 1.当需要将一个函数传递给子组件时, 最好使用useCallback进行优化, 将优化之后的函数, 传递给子组件 // props中的属性发生改变时, 组件本身就会被重新渲染 const HYHome = memo(function(props) { const { increment } = props console.log("HYHome被渲染") return ( <div> <button onClick={increment}>increment+1</button> {/* 100个子组件 */} </div> ) }) const App = memo(function() { const [count, setCount] = useState(0) const [message, setMessage] = useState("hello") // 闭包陷阱: useCallback // const increment = useCallback(function foo() { // console.log("increment") // setCount(count+1) // }, [count]) // 进一步的优化: 当count发生改变时, 也使用同一个函数(了解) // 做法一: 将count依赖移除掉, 缺点: 闭包陷阱 // 做法二: useRef, 在组件多次渲染时, 返回的是同一个值 const countRef = useRef() countRef.current = count const increment = useCallback(function foo() { console.log("increment") setCount(countRef.current + 1) }, []) // 普通的函数 // const increment = () => { // setCount(count+1) // } return ( <div> <h2>计数: {count}</h2> <button onClick={increment}>+1</button> <HYHome increment={increment}/> <h2>message:{message}</h2> <button onClick={e => setMessage(Math.random())}>修改message</button> </div> ) }) // function foo(name) { // function bar() { // console.log(name) // } // return bar // } // const bar1 = foo("why") // bar1() // why // bar1() // why // const bar2 = foo("kobe") // bar2() // kobe // bar1() // why export default App
useMemo
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) // 1.不依赖任何的值, 进行计算 const result = useMemo(() => { return calcNumTotal(50) }, []) // 2.依赖count // const result = useMemo(() => { // return calcNumTotal(count*2) // }, [count]) // 3.useMemo和useCallback的对比 function fn() {} // const increment = useCallback(fn, []) // const increment2 = useMemo(() => fn, []) // 4.使用useMemo对子组件渲染进行优化 // const info = { name: "why", age: 18 } const info = useMemo(() => ({name: "why", age: 18}), []) 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
Ref / LayoutEffect
useRef
绑定DOM:
import React, { memo, useRef } from 'react' const App = memo(() => { const titleRef = useRef() const inputRef = useRef() function showTitleDom() { console.log(titleRef.current) inputRef.current.focus() } return ( <div> <h2 ref={titleRef}>Hello World</h2> <input type="text" ref={inputRef} /> <button onClick={showTitleDom}>查看title的dom</button> </div> ) }) export default App
useRef绑定值-解决闭包陷阱:
import React, { memo, useRef } from 'react'
import { useCallback } from 'react'
import { useState } from 'react'
let obj = null
const App = memo(() => {
const [count, setCount] = useState(0)
const nameRef = useRef()
console.log(obj === nameRef)
obj = nameRef
// 通过useRef解决闭包陷阱
const countRef = useRef()
countRef.current = count
const increment = useCallback(() => {
setCount(countRef.current + 1)
}, [])
return (
<div>
<h2>Hello World: {count}</h2>
<button onClick={e => setCount(count+1)}>+1</button>
<button onClick={increment}>+1</button>
</div>
)
})
export default App
useImperativeHandleimport React, { memo, useRef, forwardRef, useImperativeHandle } from 'react' const HelloWorld = memo(forwardRef((props, ref) => { const inputRef = useRef() // 子组件对父组件传入的ref进行处理 useImperativeHandle(ref, () => { return { focus() { console.log("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.value = "" inputRef.current.setValue("哈哈哈") } return ( <div> <h2 ref={titleRef}>哈哈哈</h2> <HelloWorld ref={inputRef}/> <button onClick={handleDOM}>DOM操作</button> </div> ) }) export default App
useLayoutEffect
自定义Hooks使用
自定义Hook
打印生命周期:
import React, { memo, useEffect, useState } from 'react' function useLogLife(cName) { useEffect(() => { console.log(cName + "组件被创建") return () => { console.log(cName + "组件被销毁") } }, [cName]) } const Home = memo(() => { useLogLife("home") return <h1>Home Page</h1> }) const About = memo(() => { useLogLife("about") return <h1>About Page</h1> }) const App = memo(() => { const [isShow, setIsShow] = useState(true) useLogLife("app") return ( <div> <h1>App Root Component</h1> <button onClick={e => setIsShow(!isShow)}>切换</button> { isShow && <Home/> } { isShow && <About/> } </div> ) }) export default App
自定义Hook练习
创建Context:
import { createContext } from "react"; const UserContext = createContext() const TokenContext = createContext() export { UserContext, TokenContext }
在入口文件中设置:
import { UserContext, TokenContext } from "./12_自定义Hooks/context" import App from "./12_自定义Hooks/02_Context获取数据" const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <UserContext.Provider value={{name: "why", level: 99}}> <TokenContext.Provider value={'coderwhy'}> <App /> </TokenContext.Provider> </UserContext.Provider> );
Context抽离:
import { useContext } from "react" import { UserContext, TokenContext } from "../context" function useUserToken() { const user = useContext(UserContext) const token = useContext(TokenContext) return [user, token] } export default useUserToken
在组件中使用:
import React, { memo } from 'react' import { useUserToken } from "./hooks" // User/Token const Home = memo(() => { const [user, token] = useUserToken() return <h1>Home Page: {user.name}-{token}</h1> }) const About = memo(() => { const [user, token] = useUserToken() return <h1>About Page: {user.name}-{token}</h1> }) const App = memo(() => { return ( <div> <h1>App Root Component</h1> <Home/> <About/> </div> ) }) export default App
抽离:
import { useState, useEffect } from "react" 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] } export default useScrollPosition
在组件中使用:
import React, { memo } from 'react' import useScrollPosition from './hooks/useScrollPosition' import "./style.css" const Home = memo(() => { const [scrollX, scrollY] = useScrollPosition() return <h1>Home Page: {scrollX}-{scrollY}</h1> }) const About = memo(() => { const [scrollX, scrollY] = useScrollPosition() return <h1>About Page: {scrollX}-{scrollY}</h1> }) const App = memo(() => { return ( <div className='app'> <h1>App Root Component</h1> <Home/> <About/> </div> ) }) export default App
抽离:
在组件中使用:import { useEffect } from "react" import { useState } from "react" function useLocalStorage(key) { // 1.从localStorage中获取数据, 并且数据数据创建组件的state const [data, setData] = useState(() => { const item = localStorage.getItem(key) if (!item) return "" return JSON.parse(item) }) // 2.监听data改变, 一旦发生改变就存储data最新值 useEffect(() => { localStorage.setItem(key, JSON.stringify(data)) }, [data]) // 3.将data/setData的操作返回给组件, 让组件可以使用和修改值 return [data, setData] } export default useLocalStorage
import React, { memo } from 'react' import { useEffect } from 'react' import { useState } from 'react' import useLocalStorage from './hooks/useLocalStorage' const App = memo(() => { // 通过key, 直接从localStorage中获取一个数据 // const [token, setToken] = useState(localStorage.getItem("token")) // useEffect(() => { // localStorage.setItem("token", token) // }, [token]) const [token, setToken] = useLocalStorage("token") function setTokenHandle() { setToken("james") } // const [avatarUrl, setAvatarUrl] = useState(localStorage.getItem("avatarUrl")) // useEffect(() => { // localStorage.setItem("avatarUrl", avatarUrl) // }, [avatarUrl]) const [avatarUrl, setAvatarUrl] = useLocalStorage("avatarUrl") function setAvatarUrlHandle() { setAvatarUrl("http://www.james.com/cba.png") } return ( <div className='app'> <h1>App Root Component: {token}</h1> <button onClick={setTokenHandle}>设置token</button> <h1>avatarURL: {avatarUrl}</h1> <button onClick={setAvatarUrlHandle}>设置新头像地址</button> </div> ) }) export default App
redux hooks
useId
SSR同构应用
Hydration
useId的作用
import React, { memo, useId, useState } from 'react' const App = memo(() => { const [count, setCount] = useState(0) const id = useId() console.log(id) return ( <div> <button onClick={e => setCount(count+1)}>count+1:{count}</button> <label htmlFor={id}> 用户名:<input id={id} type="text" /> </label> </div> ) }) export default App
useTransition
js:
组件:import { faker } from '@faker-js/faker'; const namesArray = [] for (let i = 0; i < 10000; i++) { namesArray.push(faker.name.fullName()) } export default namesArray
import React, { memo, useState, useTransition } from 'react' import namesArray from './namesArray' const App = memo(() => { const [showNames, setShowNames] = useState(namesArray) const [ pending, startTransition ] = useTransition() function valueChangeHandle(event) { startTransition(() => { const keyword = event.target.value const filterShowNames = namesArray.filter(item => item.includes(keyword)) setShowNames(filterShowNames) }) } return ( <div> <input type="text" onInput={valueChangeHandle}/> <h2>用户名列表: {pending && <span>data loading</span>} </h2> <ul> { showNames.map((item, index) => { return <li key={index}>{item}</li> }) } </ul> </div> ) }) export default App
useDeferredValue
import React, { memo, useState, useDeferredValue } from 'react' import namesArray from './namesArray' const App = memo(() => { const [showNames, setShowNames] = useState(namesArray) const deferedShowNames = useDeferredValue(showNames) function valueChangeHandle(event) { const keyword = event.target.value const filterShowNames = namesArray.filter(item => item.includes(keyword)) setShowNames(filterShowNames) } return ( <div> <input type="text" onInput={valueChangeHandle}/> <h2>用户名列表: </h2> <ul> { deferedShowNames.map((item, index) => { return <li key={index}>{item}</li> }) } </ul> </div> ) }) export default App