十七、hooks
17.1 为什么使用hooks
React 没有提供将可复用性行为“附加”到组件的途径(例如,把组件连接到 store)。如果你使用过 React 一段时间,你也许会熟悉一些解决此类问题的方案,比如 render props 和 高阶组件。
Hook 使你在非 class 的情况下可以使用更多的 React 特性。 从概念上讲,React 组件一直更像是函数。而 Hook 则拥抱了函数,同时也没有牺牲 React 的精神原则。Hook 提供了问题的解决方案,无需学习复杂的函数式或响应式编程技术。
react 18版本以前hooks
zh-hans.legacy.reactjs.org/docs/hooks-…
-
useReduceruseCallbackuseMemouseRefuseImperativeHandleuseLayoutEffectuseDebugValue
17.2常见的hooks
17.2.1 useState
src/index.js
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import ErrorBoundary from './ErrorBoundary';
import App from './12_hooks/01App_useState.jsx' // hooks - useState
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<ErrorBoundary>
{/* 开启react的严格模式 */}
<React.StrictMode>
<App />
</React.StrictMode>
</ErrorBoundary>
);
src/12_hooks/01App_useState.jsx
// src/12_hooks/01App_useState.jsx
import React, { useState } from 'react';
// 数组的解构赋值
const App = () => {
// 定义了一个状态 count,初始值为10,并且定义了修改状态的函数为 setCount
const [count, setCount] = useState(10)
// const [msg, setMsg] = useState('hello world')
// const [msg] = useState('hello world')
const msg = 'hello world'
// const [obj, setObj] = useState({ a: 1, b: 2})
return (
<div>
{ msg } -
{ count }
<button onClick={ () => {
// 修改函数内直接写运行的结果
// setCount(count + 1)
// 另一种方式
setCount((prevCount) => {
return prevCount + 1
})
} }>加1</button>
</div>
);
};
export default App;
useState可以设置任何数据类型,但是实际开发过程中 需要深思熟虑,特别是使用useState创建对象时
src/index.js
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import ErrorBoundary from './ErrorBoundary';
// import App from './12_hooks/01App_useState.jsx' // hooks - useState
import App from './12_hooks/02App_useState_obj.jsx' // hooks - useState - 对象类型
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<ErrorBoundary>
{/* 开启react的严格模式 */}
<React.StrictMode>
<App />
</React.StrictMode>
</ErrorBoundary>
);
src/12_hooks/02App_useState_obj.jsx
// src/12_hooks/02App_useState_obj.jsx
import React, { useState } from 'react';
const App = () => {
const [obj, setObj] = useState({
w: 100,
h: 100,
x: 0,
y: 0
})
const move = (event) => {
// setObj({ // w, h属性丢失
// x: event.clientX,
// y: event.clientY
// })
// setObj({ // 正确
// w: obj.w,
// h: obj.h,
// x: event.clientX,
// y: event.clientY
// })
// 对象合并
// setObj(Object.assign({}, obj, { x: event.clientX, y: event.clientY }))
setObj({ ...obj, x: event.clientX, y: event.clientY })
}
return (
<div style={ { width: '100vw', height: '100vh', backgroundColor: '#ccc' } } onMouseMove={ move }>
<div style={ { width: obj.w, height: obj.h, backgroundColor: '#f66' }}></div>
鼠标位置为(x: { obj.x }, y: { obj.y })
</div>
);
};
export default App;
思考:如上效果虽然已经实现,但是设计合理吗
src/index.js
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import ErrorBoundary from './ErrorBoundary';
// import App from './12_hooks/01App_useState.jsx' // hooks - useState
// import App from './12_hooks/02App_useState_obj.jsx' // hooks - useState - 对象类型
import App from './12_hooks/03App_useState_mutiple_obj.jsx' // hooks - useState - 对象类型 - 分开写
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<ErrorBoundary>
{/* 开启react的严格模式 */}
<React.StrictMode>
<App />
</React.StrictMode>
</ErrorBoundary>
);
src/12_hooks/03App_useState_mutiple_obj.jsx
// src/12_hooks/03App_useState_mutiple_obj.jsx
import React, {useState} from 'react';
const App = () => {
const [box] = useState({ w: 200, h: 200 })
const [position, setPosition] = useState({ x: 0, y: 0})
const move = (event) => {
setPosition({
x: event.clientX,
y: event.clientY
})
}
return (
<div style={ { width: '100vw', height: '100vh', backgroundColor: '#ccc' } } onMouseMove={ move }>
<div style={ { width: box.w, height: box.h, backgroundColor: '#f66' }}></div>
鼠标位置为(x: { position.x }, y: { position.y })
</div>
);
};
export default App;
不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层以及任何 return 之前调用他们。
只在 React 函数中调用 Hook, 不要在普通的 JavaScript 函数中调用 Hook。你可以:
- 在 React 的函数组件中调用 Hook
- 在自定义 Hook 中调用其他 Hook
如果遇到需要以对象的形式定义状态时,根据需求划分对象,因为修改状态使用的是替换
17.2.2 useEffect
Effect Hook 可以让你在函数组件中执行副作用操作
可以是 componentDidMount() 、componentDidUpdate() 、componentWillUnmount的合体:
src/index.js
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import ErrorBoundary from './ErrorBoundary';
// import App from './12_hooks/01App_useState.jsx' // hooks - useState
// import App from './12_hooks/02App_useState_obj.jsx' // hooks - useState - 对象类型
// import App from './12_hooks/03App_useState_mutiple_obj.jsx' // hooks - useState - 对象类型 - 分开写
import App from './12_hooks/04App_useEffect.jsx' // hooks - useEffect
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<ErrorBoundary>
{/* 开启react的严格模式 */}
<React.StrictMode>
<App />
</React.StrictMode>
</ErrorBoundary>
);
src/12_hooks/04App_useEffect.jsx
// src/12_hooks/04App_useEffect.jsx
import React, { useEffect, useState } from 'react';
const App = () => {
// onMounted(() => {}) // 写法类比vue中的钩子函数
// useEffect(() => {}) // 相当于componentDidMount + componentDidUpdate
// useEffect(() => {}, []) // 相当于componentDidMount
// useEffect(() => {}, [num]) // 相当于componentDidMount + componentDidUpdate(条件) - 侦听num的变化执行
// useEffect(() => { return () => {}}, [num]) // 相当于componentDidMount + componentDidUpdate(条件) + componentWillUnmount(返回的函数)
// useEffect(() => { return () => {} }, []) // 相当于componentDidMount + componentWillUnmount(返回的函数)
// useEffect(() => { return () => {} }) // 相当于componentDidMount + componentDidUpdate + componentWillUnmount(返回的函数)
const [list, setList] = useState([])
const [count, setCount] = useState(10)
// useEffect(() => { // 数据请求一直在执行
// fetch('http://121.89.205.189:3000/api/pro/list').then(res => res.json()).then(res => {
// console.log(res.data)
// setList(res.data)
// })
// })
// useEffect(() => { // 添加一个空的依赖项,保证只执行一次
// fetch('http://121.89.205.189:3000/api/pro/list').then(res => res.json()).then(res => {
// console.log(res.data)
// setList(res.data)
// })
// }, [])
useEffect(() => { // 添加依赖项,依赖的值发生变化就会执行一次
fetch('http://121.89.205.189:3000/api/pro/list').then(res => res.json()).then(res => {
console.log(res.data)
setList(res.data)
})
return () => {
console.log('组件销毁')
}
}, [count])
// 通常,组件卸载时需要清除 effect 创建的诸如订阅或计时器 ID 等资源。
// 要实现这一点,传递给 useEffect 的函数需返回一个清除函数。
// 为防止内存泄漏,清除函数会在组件卸载前执行。
// 另外,如果组件多次渲染(通常如此),则在执行下一个 effect 之前,上一个 effect 就已被清除。
/**
* useEffect(() => {
const subscription = props.source.subscribe(); // 订阅
return () => {
// 清除订阅
subscription.unsubscribe();
};
});
*/
const [timer, setTimer] = useState(null)
useEffect(() => {
const test = setInterval(() => {
console.log(count)
}, 1000)
setTimer(test) // componentDidMount
// 订阅
return () => {
console.log('666')
clearInterval(timer) // componentWillUnmount
// 取消订阅
}
}, [count])
/**
* state = {
* timer: null
* }
* componentDidMount () {
* const test = setInterval(() => {
console.log(count)
}, 1000)
this.setState({ timer: test })
// 订阅数据
* }
componentWillUnmount () {
clearInterval(this.state.timer)
// 取消订阅
}
*/
// useEffect 函数内部的返回的函数 可用可不用,类似于componentWillUnmount
return (
<div>
<button onClick={() => setCount(count + 1)}>加1</button>
<ul>
{
list && list.map(item => {
return (
<li key = { item.proid }>
{ item.proname }
</li>
)
})
}
</ul>
</div>
);
};
export default App;
- useEffect() 是个副作用函数。
- useEffect() 函数在每次组件重新渲染时,可再次被调用。
- 在开发环境中,开启了 React.StrictMode 模式,组件开始时被渲染两次。zh-hans.legacy.reactjs.org/docs/strict…
- useEffect() 通过返回函数来清理副作用。
- useEffect() 通过传递第二个参数数组来提高渲染性能,或者说实现 watch 效果。
17.2.3 useRef
利用 useRef 就可以绕过 Capture Value 的特性。可以认为 ref 在所有 Render 过程中保持着唯一引用,因此所有对 ref 的赋值或取值,拿到的都只有一个最终状态,而不会在每个 Render 间存在隔离。
src/index.js
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import ErrorBoundary from './ErrorBoundary';
// import App from './12_hooks/01App_useState.jsx' // hooks - useState
// import App from './12_hooks/02App_useState_obj.jsx' // hooks - useState - 对象类型
// import App from './12_hooks/03App_useState_mutiple_obj.jsx' // hooks - useState - 对象类型 - 分开写
// import App from './12_hooks/04App_useEffect.jsx' // hooks - useEffect
import App from './12_hooks/05App_useRef.jsx' // hooks - useRef
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<ErrorBoundary>
{/* 开启react的严格模式 */}
<React.StrictMode>
<App />
</React.StrictMode>
</ErrorBoundary>
);
src/12_hooks/05App_useRef.jsx
// src/12_hooks/05App_useRef.jsx
import React, { forwardRef, useEffect, useRef } from 'react';
const Child = forwardRef((props, ref) => {
return (
<div ref = { ref }>
child
</div>
)
})
const App = () => {
const testRef = useRef()
useEffect(() => {
console.log(testRef.current)
}, [])
return (
<div>
<Child ref = { testRef }></Child>
</div>
);
};
export default App;
17.2.4 useReducer
useReducer 践行了 Flux/Redux 思想。使用步骤:
1、创建初始值initialState
2、创建所有操作 reducer(state, action);
3、传给 userReducer,得到读和写API
4、调用写 ({type: '操作类型'})
总的来说,useReducer 是 useState 的复杂版。
useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。(如果你熟悉 Redux 的话,就已经知道它如何工作了。)
在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数 。
src/index.js
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import ErrorBoundary from './ErrorBoundary';
// import App from './12_hooks/01App_useState.jsx' // hooks - useState
// import App from './12_hooks/02App_useState_obj.jsx' // hooks - useState - 对象类型
// import App from './12_hooks/03App_useState_mutiple_obj.jsx' // hooks - useState - 对象类型 - 分开写
// import App from './12_hooks/04App_useEffect.jsx' // hooks - useEffect
// import App from './12_hooks/05App_useRef.jsx' // hooks - useRef
import App from './12_hooks/06App_useReducer.jsx' // hooks - useReducer
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<ErrorBoundary>
{/* 开启react的严格模式 */}
<React.StrictMode>
<App />
</React.StrictMode>
</ErrorBoundary>
);
src/12_hooks/06App_useReducer.jsx
// src/12_hooks/06App_useReducer.jsx
import React, { useEffect, useReducer } from 'react';
/**
*
1、创建初始值initialState
2、创建所有操作 reducer(state, action);
3、传给 userReducer,得到读和写API
4、调用写 ({type: '操作类型'})
*/
// 1、创建初始值initialState
const initialState = {
bannerList: [],
proList: [],
count: 10
}
// 2. 创建所有操作 reducer(state, action); 定义好修改状态的reducer - 纯函数
// state 初始化状态以及上一次的状态
// action 包含修改状态的标识以及传递的数据 { type: '', payload: '' }
const reducer = (state, action) => {
switch (action.type) {
case 'CHANGE_BANNER_LIST':
return { ...state, bannerList: action.payload }
case 'CHANGE_PRO_LIST':
return Object.assign({}, state, { proList: action.payload })
case 'ADD_COUNT':
return Object.assign({}, state, { count: state.count + action.payload })
default:
return state
}
}
const Child = () => {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<div>
<h1>child</h1>
<button onClick={ () => {
dispatch({
type: 'ADD_COUNT',
payload: 10
})
} }>加10</button>
{ state.count }
</div>
)
}
const App = () => {
// 3、传给 userReducer,得到读和写API
// state 就是状态,dispatch 触发更改状态的操作
const [state, dispatch] = useReducer(reducer, initialState)
useEffect(() => {
fetch('http://121.89.205.189:3000/api/banner/list').then(res => res.json()).then(res => {
// 修改状态
dispatch({
type: 'CHANGE_BANNER_LIST',
payload: res.data
})
})
}, [])
useEffect(() => {
fetch('http://121.89.205.189:3000/api/pro/list').then(res => res.json()).then(res => {
// 修改状态
dispatch({
type: 'CHANGE_PRO_LIST',
payload: res.data
})
})
}, [])
return (
<div>
<button onClick={ () => {
dispatch({
type: 'ADD_COUNT',
payload: 5
})
} }>加5</button> { state.count }
{
state.bannerList && state.bannerList.map(item => {
return <img style={{ height: 60 }} src={ item.img } key = { item.bannerid } alt={ item.alt } />
})
}
<ul>
{
state.proList && state.proList.map(item => {
return (
<li key = { item.proid }>{ item.proname }</li>
)
})
}
</ul>
<Child />
</div>
);
};
export default App;
如果遇到多个组件需要共享状态时,单纯useReducer就显得无能为力,每次都是获取的初始化数据
以上案例可以将Child组件看作是App组件的子组件,可以实现状态共享 ---- 父子组件传值,不适合 非亲兄弟组件传值
// src/12_hooks/06App_useReducer.jsx import React, { useEffect, useReducer } from 'react'; /** * 1、创建初始值 initialState 2、创建所有操作 reducer(state, action); 3、传给 userReducer,得到读和写API 4、调用写 ({type: '操作类型'}) */ // 1、创建初始值 initialState const initialState = { bannerList: [], proList: [], count: 10 } // 2、创建所有操作 reducer(state, action); // 纯函数 state 上一次的状态 action 行为以及数据 const reducer = (state, action) => { switch (action.type) { case 'CHANGE_BANNER_LIST': return { ...state, bannerList: action.payload } case 'CHANGE_PRO_LIST': return { ...state, proList: action.payload } case 'ADD_COUNT': return { ...state, count: state.count + action.payload } default: return state } } const Child = (props) => { // const [state, dispatch] = useReducer(reducer, initialState) return ( <> <button onClick={() => { props.dispatch({ type: 'ADD_COUNT', payload: 5 }) }}>加5</button> { props.count } </> ) } const App = () => { // 3、传给 userReducer,得到读和写API const [state, dispatch] = useReducer(reducer, initialState) const { bannerList, proList, count } = state // const [{ bannerList, proList, count }, dispatch] = useReducer(reducer, initialState) useEffect(() => { fetch('http://121.89.205.189:3000/api/banner/list').then(res => res.json()).then(res => { // 修改状态 dispatch({ type: 'CHANGE_BANNER_LIST', payload: res.data }) }) fetch('http://121.89.205.189:3000/api/pro/list').then(res => res.json()).then(res => { // 修改状态 dispatch({ type: 'CHANGE_PRO_LIST', payload: res.data }) }) }, []) return ( <div> <button onClick={() => { dispatch({ type: 'ADD_COUNT', payload: 10 }) }}>加10</button> { count } { bannerList && bannerList.map(item => { return <img style={{ height: 60 }} src={ item.img } key = { item.bannerid } alt={ item.alt }/> }) } <ul> { proList && proList.map(item => { return ( <li key = { item.proid }>{ item.proname }</li> ) }) } </ul> <Child count = { count } dispatch = { dispatch }/> {/* <Child1/> <Child2 /> */ } </div> ); }; export default App;配合useContext 实现全局的状态管理
17.2.5 useContext
1、使用 C = createContext(initial) 创建上下文
2、使用 <C.Provider> 圈定作用域
3、在作用域内使用 useContext(C)来使用上下文
src/index.js
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import ErrorBoundary from './ErrorBoundary';
// import App from './12_hooks/01App_useState.jsx' // hooks - useState
// import App from './12_hooks/02App_useState_obj.jsx' // hooks - useState - 对象类型
// import App from './12_hooks/03App_useState_mutiple_obj.jsx' // hooks - useState - 对象类型 - 分开写
// import App from './12_hooks/04App_useEffect.jsx' // hooks - useEffect
// import App from './12_hooks/05App_useRef.jsx' // hooks - useRef
// import App from './12_hooks/06App_useReducer.jsx' // hooks - useReducer
import App from './12_hooks/07App_useContext.jsx' // hooks - useContext
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<ErrorBoundary>
{/* 开启react的严格模式 */}
<React.StrictMode>
<App />
</React.StrictMode>
</ErrorBoundary>
);
src/12_hooks/01App_useContext.jsx
// src/12_hooks/07App_useContext.jsx
import React, { useContext, createContext } from 'react';
// 1.创建上下文对象
const MyContext = createContext()
const ColorContext = createContext()
MyContext.displayName = 'MyContext'
ColorContext.displayName = 'ColorContext'
const Second = () => {
const msg = useContext(MyContext) // hooks取值
const color = useContext(ColorContext)
return (
<>
<div>
2222 - { msg } - { color }
</div>
</>
)
}
const First = () => {
return (
<div>
<Second ></Second>
</div>
)
}
const App = () => {
const msg = '传家宝'
return (
<div>
<MyContext.Provider value = { msg }>
<ColorContext.Provider value="red">
<First ></First>
</ColorContext.Provider>
</MyContext.Provider>
</div>
)
}
export default App;
使用 useReducer 和 useContext 实现轻型Redux,可以让组件间共享状态
步骤:
1、将数据集中在一个 store 对象
2、将所有操作集中在 reducer
3、创建一个 Context
4、创建对数据的读取 API
5、将第四步的内容放到第三步的 Context
6、用 Context.Provider 将 Context 提供给所有组件
7、各个组件用 useContext 获取读写API
src/index.js
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import ErrorBoundary from './ErrorBoundary';
// import App from './12_hooks/01App_useState.jsx' // hooks - useState
// import App from './12_hooks/02App_useState_obj.jsx' // hooks - useState - 对象类型
// import App from './12_hooks/03App_useState_mutiple_obj.jsx' // hooks - useState - 对象类型 - 分开写
// import App from './12_hooks/04App_useEffect.jsx' // hooks - useEffect
// import App from './12_hooks/05App_useRef.jsx' // hooks - useRef
// import App from './12_hooks/06App_useReducer.jsx' // hooks - useReducer
// import App from './12_hooks/07App_useContext.jsx' // hooks - useContext
import App from './12_hooks/08App_little_redux.jsx' // hooks - useContext + useReducer 轻型redux
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<ErrorBoundary>
{/* 开启react的严格模式 */}
<React.StrictMode>
<App />
</React.StrictMode>
</ErrorBoundary>
);
src/08App_little_redux.jsx
// src/12_hooks/08App_little_redux.jsx
import React, { createContext, useContext, useReducer } from 'react';
// 4.创建上下文对象,起名字
const MyContext = createContext()
MyContext.displayName = 'MyContext'
// 1、创建初始值 initialState
const initialState = { // 将所有需要共享的状态都放在一起
count: 10
}
// 2、创建所有操作 reducer(state, action);
// 纯函数 state 上一次的状态 action 行为以及数据
const reducer = (state, { type, payload }) => {
switch (type) {
case 'ADD':
return { ...state, count: state.count + payload }
case 'REDUCE':
return { ...state, count: state.count - payload }
default:
return state
}
}
const Page2 = () => {
// 6.组件内部使用 useContext 获取到 组件组件传递的状态以及触发修改状态的函数dispatch
const { state, dispatch } = useContext(MyContext)
return (
<div>
<h1>第二个页面</h1>
<button onClick={() => {
dispatch({
type: 'REDUCE',
payload: 5
})
}}>-5</button> { state.count } <button onClick={() => {
dispatch({
type: 'ADD',
payload: 5
})
}}>+5</button>
</div>
)
}
const Page1 = () => {
// 6.组件内部使用 useContext 获取到 组件组件传递的状态以及触发修改状态的函数dispatch
const { state, dispatch } = useContext(MyContext)
return (
<div>
<h1>第一个页面</h1>
<button onClick={() => {
dispatch({
type: 'REDUCE',
payload: 10
})
}}>-10</button> { state.count } <button onClick={() => {
dispatch({
type: 'ADD',
payload: 10
})
}}>+10</button>
</div>
)
}
const App = () => {
// 3.顶级组件通过 userReducer,得到读和写API
const [state, dispatch] = useReducer(reducer, initialState)
return (
<div>
{/* 5.通过上下文对象.Provider组件,结合value属性给后代组件传值 */}
<MyContext.Provider value = { { state, dispatch }}>
<Page1></Page1>
<Page2></Page2>
</MyContext.Provider>
</div>
);
};
export default App;
useContext + useReducer 可以实现轻型redux,但是不适合应用于多模块管理的大型项目
17.2.6 useMemo
把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。
如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值
你可以把 useMemo 作为性能优化的手段,但不要把它当成语义上的保证。
17.2.7 useCallback
把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。
src/index.js
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import ErrorBoundary from './ErrorBoundary';
// import App from './12_hooks/01App_useState.jsx' // hooks - useState
// import App from './12_hooks/02App_useState_obj.jsx' // hooks - useState - 对象类型
// import App from './12_hooks/03App_useState_mutiple_obj.jsx' // hooks - useState - 对象类型 - 分开写
// import App from './12_hooks/04App_useEffect.jsx' // hooks - useEffect
// import App from './12_hooks/05App_useRef.jsx' // hooks - useRef
// import App from './12_hooks/06App_useReducer.jsx' // hooks - useReducer
// import App from './12_hooks/07App_useContext.jsx' // hooks - useContext
// import App from './12_hooks/08App_little_redux.jsx' // hooks - useContext + useReducer 轻型redux
import App from './12_hooks/09_App_hooks_useCallback_useMemo.jsx' // hooks - useCallback useMemo
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<ErrorBoundary>
{/* 开启react的严格模式 */}
<React.StrictMode>
<App />
</React.StrictMode>
</ErrorBoundary>
);
src/12_hooks/09_App_hooks_useCallback_useMemo.jsx
// src/12_hooks/09_App_hooks_useCallback_useMemo.jsx
import React, { memo, useCallback, useMemo, useState } from 'react';
// 如果子组件是函数式组件,且调用组件时没有传递任何依赖项
// 想要达到子组件不重新渲染,需要使用高阶组件 React.memo 包裹组件
// 类组件继承 PureComponent 解决问题
const Child = memo(() => {
console.log('child 被执行')
return (
<div>
Child
</div>
)
})
const Child1 = memo(() => {
console.log('child1 被执行')
return (
<div>
Child1
</div>
)
})
const Child2 = memo(() => {
console.log('child2 被执行')
return (
<div>
Child2
</div>
)
})
const App = () => {
const [count, setCount] = useState(10)
const [arr, setArr] = useState([1, 2, 3, 4, 5])
// const handler = (event) => {} // 每次更改 count 的值,那么 child2 被执行 被打印
// const handler = useCallback(() => { // 每次更改 count 的值,那么 child2 被执行 没有被打印 --- 记忆函数
// return (event) => {}
// }, [])
const handler = useMemo((event) => {}, []) // 每次更改 count 的值,那么 child2 被执行 没有被打印 --- 记忆函数
// useMemo 还可以充当计算属性
const doubleCount = count * 2
const len = useMemo(() => { // 计算属性,arr数据发生过变化,重新取值
console.log('computed', arr)
return arr.length
}, [arr])
return (
<div>
<button onClick={() => setCount(count + 1)}>加1</button> { count } - { doubleCount }
<button onClick={ () => {
const oldArr = JSON.parse(JSON.stringify(arr)) // 深拷贝
oldArr.push(arr.length + 1)
setArr(oldArr)
console.log(arr)
}}>追加数据</button> - { arr.length } - { len }
{
arr.map(item => {
return <p key={ item }>{item}</p>
})
}
<Child />
<Child1 count = { count }/>
<Child2 handler = { handler }/>
</div>
);
};
export default App;
useCallback 应用于 组件的事件,使用useCallback 包裹组件 - 记忆函数
useMemo 可以是计算属性,
useMemo 也应用于 组件的事件 - - 记忆函数,
使用useMemo 包裹组件 - 记忆组件
const Child = () => {}
const ChildCom = useMemo(() =>Child , [])
17.2.8 useImperativeHandle
useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。
src/index.js
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
// import App from './12_hooks/01App_useState'
// import App from './12_hooks/02App_useState'
// import App from './12_hooks/03App_useState'
// import App from './12_hooks/04App_useEffect'
// import App from './12_hooks/05App_useRef'
// import App from './12_hooks/06App_useReducer'
// import App from './12_hooks/07App_useContext'
// import App from './12_hooks/08App_little_redux'
// import App from './12_hooks/09App_useCallback_useMemo'
// import App from './12_hooks/10App_useCallback_useMemo'
import App from './12_hooks/11App_useImperativeHandle'
import ErrorBoundary from './ErrorBoundary'
// import './style.stylus'
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<ErrorBoundary>
{/* react严格模式的组件 */}
{/* <React.StrictMode> */}
<App root={ root }/>
{/* </React.StrictMode> */}
</ErrorBoundary>
)
src/12_hooks/11App_useImperativeHandle.jsx
// src/12_hooks/11App_useImperativeHandle.jsx
// 在父组件中获取子组件提供的数据何方法
import React, { forwardRef, useImperativeHandle, useRef } from 'react';
const MyInput = forwardRef((props, ref) => {
const inputRef = useRef()
// 关联了父组件传递过来的 ref 值
// 暴露了父组件可以访问的子组件的数据何方法
useImperativeHandle(ref, () => {
return {
a: '111',
b: 222,
focus: () => {
inputRef.current.focus()
}
}
})
return (
<div>
<input type="text" ref = { inputRef }/>
</div>
)
})
const App = () => {
const myInputRef = useRef()
const getData = () => {
console.log(myInputRef.current.a)
console.log(myInputRef.current.b)
myInputRef.current.focus()
}
return (
<div>
<button onClick={ getData }>获取子组件的数据以及获取焦点</button>
<MyInput ref = { myInputRef }/>
</div>
);
};
export default App;
上面这个例子中与直接转发 ref 不同,直接转发 ref 是将 React.forwardRef 中函数上的 ref 参数直接应用在了返回元素的 ref 属性上,其实父、子组件引用的是同一个 ref 的 current 对象,官方不建议使用这样的 ref 透传,而使用 useImperativeHandle 后,可以让父、子组件分别有自己的 ref,通过 React.forwardRef 将父组件的 ref 透传过来,通过 useImperativeHandle 方法来自定义开放给父组件的 current。
useImperativeHandle的第一个参数是定义 current 对象的 ref,第二个参数是一个函数,返回值是一个对象,即这个 ref 的 current 对象,这样可以像上面的案例一样,通过自定义父组件的 ref 来使用子组件 ref 的某些方法。
useImperativeHandle 和React.forwardRef必须是配合使用的,这也是为什么在开头要介绍 ref 的转发。
17.2.9 useLayoutEffect
useLayoutEffect 与 useEffect的区别:
useEffect是异步执行的,而useLayoutEffect是同步执行的。useEffect的执行时机是浏览器完成渲染之后,而useLayoutEffect的执行时机是浏览器把内容真正渲染到界面之前
举个例子:
src/index.js
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
// import App from './12_hooks/01App_useState'
// import App from './12_hooks/02App_useState'
// import App from './12_hooks/03App_useState'
// import App from './12_hooks/04App_useEffect'
// import App from './12_hooks/05App_useRef'
// import App from './12_hooks/06App_useReducer'
// import App from './12_hooks/07App_useContext'
// import App from './12_hooks/08App_little_redux'
// import App from './12_hooks/09App_useCallback_useMemo'
// import App from './12_hooks/10App_useCallback_useMemo'
// import App from './12_hooks/11App_useImperativeHandle'
import App from './12_hooks/12App_useLayoutEffect'
import ErrorBoundary from './ErrorBoundary'
// import './style.stylus'
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<ErrorBoundary>
{/* react严格模式的组件 */}
{/* <React.StrictMode> */}
<App root={ root }/>
{/* </React.StrictMode> */}
</ErrorBoundary>
)
src/12App_useLayoutEffect.jsx
// src/12_hooks/12App_useLayoutEffect.jsx
import React, { useEffect, useLayoutEffect, useRef } from 'react';
const App = () => {
const inputRef = useRef()
useEffect(() => { // DOM操作 数据请求
console.log('useEffect') // 后打印
inputRef.current.value = '1111'
}, [])
useLayoutEffect(() => { // DOM操作
console.log('useLayoutEffect') // 先打印
inputRef.current.value = '2222'
})
return (
<div>
<input type="text" ref={ inputRef } />
</div>
);
};
export default App;
useLayoutEffect 做DOM操作
useEffect 中 副作用执行
17.2.10 useDebugValue
useDebugValue 可用于在 React 开发者工具中显示自定义 hook 的标签。
src/index.js
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
// import App from './12_hooks/01App_useState'
// import App from './12_hooks/02App_useState'
// import App from './12_hooks/03App_useState'
// import App from './12_hooks/04App_useEffect'
// import App from './12_hooks/05App_useRef'
// import App from './12_hooks/06App_useReducer'
// import App from './12_hooks/07App_useContext'
// import App from './12_hooks/08App_little_redux'
// import App from './12_hooks/09App_useCallback_useMemo'
// import App from './12_hooks/10App_useCallback_useMemo'
// import App from './12_hooks/11App_useImperativeHandle'
// import App from './12_hooks/12App_useLayoutEffect'
import App from './12_hooks/13App_useDebugValue'
import ErrorBoundary from './ErrorBoundary'
// import './style.stylus'
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<ErrorBoundary>
{/* react严格模式的组件 */}
{/* <React.StrictMode> */}
<App root={ root }/>
{/* </React.StrictMode> */}
</ErrorBoundary>
)
src/12_hooks/13App_useDebugValue.jsx
// src/12_hooks/13App_useDebugValue.jsx
import React, { useDebugValue, useEffect, useState } from 'react';
// const App = () => {
// const [count, setCount] = useState(0)
// const add = () => setCount(count + 1)
// useEffect(() => {
// document.title = `点击了${count}次`
// })
// return (
// <div>
// <button onClick={ add }>加1</button> { count }
// </div>
// );
// };
const useCount = () => {
useDebugValue('myCount')
const [count, setCount] = useState(0)
const add = () => setCount(count + 1)
return {
count, add
}
}
const useTitle = (count) => {
useDebugValue('myTitle')
useEffect(() => {
document.title = `点击了${count}次`
})
}
const App = () => {
const { count, add } = useCount()
useTitle(count)
return (
<div>
<button onClick={ add }>加1</button> { count }
</div>
);
}
export default App;
接下来的hooks是属于react18版本新增的hooks
17.2.11 useId
useId是一个钩子,用于生成唯一的ID,在服务器和客户端之间是稳定的,同时避免hydration 不匹配。
注意:
useId不是用来生成列表中的键的。Keys应该从你的数据中生成。不能用于列表渲染的key值
对于一个基本的例子,直接将id传递给需要它的元素。
对于同一组件中的多个ID,使用相同的ID附加一个后缀。
src/index.js
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
// import App from './12_hooks/01App_useState'
// import App from './12_hooks/02App_useState'
// import App from './12_hooks/03App_useState'
// import App from './12_hooks/04App_useEffect'
// import App from './12_hooks/05App_useRef'
// import App from './12_hooks/06App_useReducer'
// import App from './12_hooks/07App_useContext'
// import App from './12_hooks/08App_little_redux'
// import App from './12_hooks/09App_useCallback_useMemo'
// import App from './12_hooks/10App_useCallback_useMemo'
// import App from './12_hooks/11App_useImperativeHandle'
// import App from './12_hooks/12App_useLayoutEffect'
// import App from './12_hooks/13App_useDebugValue'
import App from './12_hooks/14App_useId'
import ErrorBoundary from './ErrorBoundary'
// import './style.stylus'
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<ErrorBoundary>
{/* react严格模式的组件 */}
{/* <React.StrictMode> */}
<App root={ root }/>
{/* </React.StrictMode> */}
</ErrorBoundary>
)
src/12_hooks/14App_useId.jsx
// src/12_hooks/14App_useId.jsx
import React, { useId, useState } from 'react';
const App = () => {
const [list] = useState(['aa', 'bb', 'cc'])
const useNameId = useId()
const passwordId = useId()
return (
<div>
{
// list.map(item => { // ❌
// const id = useId() // 不可以在jsx代码中使用 hooks
// return <p key={ id }>{ item }</p>
// })
list.map(item => {
return <p key={ item }>{ item }</p>
})
}
<form >
<div>
{/* 网页中为 for属性 ==》 react htmlFor */}
{/* <label htmlFor="userName">用户名</label>
<input type="text" id='userName' /> */}
<label htmlFor={ useNameId }>用户名</label>
<input type="text" id={ useNameId } />
</div>
<div>
<label htmlFor={ passwordId }>密码</label>
<input type="password" id={ passwordId } />
</div>
</form>
</div>
);
};
export default App;
注意:
useId会生成一个包含 : token的字符串。这有助于确保令牌是唯一的,但在CSS选择器或API(如querySelectorAll)中不支持。
useId支持一个identifierPrefix,以防止在多根应用程序中发生碰撞。要配置,请参阅hydrateRoot和ReactDOMServer的选项。hooks需要在函数式组件以及自定义hook的顶级使用(返回jsx代码之前),不要在jsx代码中使用hooks
17.2.12 useDeferredValue - 暂时了解
真实需求其实不需要实时渲染所有的数据
useDeferredValue 需要接收一个值, 返回这个值的副本, 副本的更新会在值更新渲染之后进行更新, 以此来避免一些不必要的重复渲染. 打个比方页面中有输入框, 输入框下的内容依赖于输入框的值, 但是输入框是一个高频操作, 如果输入10次, 可能用户只想看到最终的结果那么中途的实时渲染就显得不那么重要了, 页面元素少点还好, 一旦元素过多页面就会及其的卡顿, 渲染引擎堵得死死的, 用户就会骂娘了, 此时使用useDeferredValue是一个很好的选择
src/index.js
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
// import App from './12_hooks/01App_useState'
// import App from './12_hooks/02App_useState'
// import App from './12_hooks/03App_useState'
// import App from './12_hooks/04App_useEffect'
// import App from './12_hooks/05App_useRef'
// import App from './12_hooks/06App_useReducer'
// import App from './12_hooks/07App_useContext'
// import App from './12_hooks/08App_little_redux'
// import App from './12_hooks/09App_useCallback_useMemo'
// import App from './12_hooks/10App_useCallback_useMemo'
// import App from './12_hooks/11App_useImperativeHandle'
// import App from './12_hooks/12App_useLayoutEffect'
// import App from './12_hooks/13App_useDebugValue'
// import App from './12_hooks/14App_useId'
import App from './12_hooks/15App_useDeferredValue'
import ErrorBoundary from './ErrorBoundary'
// import './style.stylus'
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<ErrorBoundary>
{/* react严格模式的组件 */}
{/* <React.StrictMode> */}
<App root={ root }/>
{/* </React.StrictMode> */}
</ErrorBoundary>
)
src/12_hooks/15App_useDeferredValue.jsx 数据量必须过大,否则看不到效果
// src/12_hooks/15App_useDeferredValue.jsx
import React, { useDeferredValue, useEffect, useState, useMemo, memo } from 'react'
const List = memo(function List({count}) {
const [data, setData] = useState([])
useEffect(() => {
const data = []
data.length = 50000
for (let i = 0; i < data.length; i++) {
data.fill(i+1, i)
}
setData(data)
}, [count])
return (
<div>
{
data.map((item) => {
return (
<p key={item}>{count}</p>
)
})
}
</div>
)
})
export default function UseDeferredValueDemo() {
const [inpVal, setInpVal] = useState('')
const deferredValue = useDeferredValue(inpVal) // 备份数据
const memoList = useMemo(() => <List count={deferredValue}></List>, [deferredValue])
return (
<>
<h1>UseDeferredValue</h1>
<input type="text" value={inpVal} onChange={(e) => setInpVal(e.target.value)}/>
{memoList}
</>
)
}
17.2.13 useTransition - 暂时了解
useTransition 又叫过渡, 他的作用就是标记非紧急更新, 这些被标记非紧急更新会在紧急更新完之后进行更新, useTransition 使用场景在应对渲染量很大的页面,需要及时响应某些事件的情况。
举个例子,准备一个进度条, 通过滑动进度条来显示进度条的进度并且渲染相同进度数量的div, 如果我们不对渲染进行优化那无疑页面会很卡, 此时使用过渡配合useMemo来缓存页面结构, diffing算法就会对比出少量的变化进行局部修改。
src/index.js
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
// import App from './12_hooks/01App_useState'
// import App from './12_hooks/02App_useState'
// import App from './12_hooks/03App_useState'
// import App from './12_hooks/04App_useEffect'
// import App from './12_hooks/05App_useRef'
// import App from './12_hooks/06App_useReducer'
// import App from './12_hooks/07App_useContext'
// import App from './12_hooks/08App_little_redux'
// import App from './12_hooks/09App_useCallback_useMemo'
// import App from './12_hooks/10App_useCallback_useMemo'
// import App from './12_hooks/11App_useImperativeHandle'
// import App from './12_hooks/12App_useLayoutEffect'
// import App from './12_hooks/13App_useDebugValue'
// import App from './12_hooks/14App_useId'
// import App from './12_hooks/15App_useDeferredValue'
import App from './12_hooks/16App_useTransition'
import ErrorBoundary from './ErrorBoundary'
// import './style.stylus'
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<ErrorBoundary>
{/* react严格模式的组件 */}
{/* <React.StrictMode> */}
<App root={ root }/>
{/* </React.StrictMode> */}
</ErrorBoundary>
)
src/12_hooks/16App_useTransition
import React, { useTransition, useState, useMemo } from 'react'
export default function UseTransition() {
const [isPending, startTransition] = useTransition()
const [rangeValue, setRangeValue] = useState(1)
const [renderData, setRenderData] = useState([1])
const [isStartTransition, setIsStartTransition] = useState(false)
const handleChange = (e) => {
setRangeValue(e.target.value)
const arr = []
arr.length = e.target.value
for (let i = 0; i <= arr.length; i++) {
arr.fill(i, i + 1)
}
if (isStartTransition) {
startTransition(() => {
setRenderData(arr)
})
} else {
setRenderData(arr)
}
}
const jsx = useMemo(() => {
return renderData.map((item, index) => {
return (
<div
style={{
width: 50,
height: 50,
backgroundColor: `#${Math.floor(Math.random() * 16777215).toString(
16
)}`,
margin: 10,
display: 'inline-block',
}}
key={'item'+index}
>
{item}
</div>
)
})
}, [renderData])
return (
<div>
<div style={{ textAlign: 'center' }}>
<label>
<input
type="checkbox"
checked={isStartTransition}
onChange={(e) => {
setIsStartTransition(e.target.checked)
}}
/>
useTransition
</label>
<input
type="range"
value={rangeValue}
min={0}
max={10000}
style={{ width: 120 }}
onChange={handleChange}
/>
<span>进度条 {rangeValue}</span>
<hr />
</div>
{jsx}
</div>
)
}
import React, { useTransition, useState, useMemo } from 'react'
export default function UseTransition() {
const [isPending, startTransition] = useTransition()
// 滑块默认值
const [rangeValue, setRangeValue] = useState(1)
const [renderData, setRenderData] = useState([1])
const [isStartTransition, setIsStartTransition] = useState(false)
// 滑块滑动事件
const handleChange = (e) => {
setRangeValue(e.target.value)
const arr = []
arr.length = e.target.value
for (let i = 0; i <= arr.length; i++) {
arr.fill(i, i + 1)
}
if (isStartTransition) {
startTransition(() => {
setRenderData(arr)
})
} else {
setRenderData(arr)
}
}
const jsx = useMemo(() => {
return renderData.map((item, index) => {
return (
<div
style={{
width: 50,
height: 50,
backgroundColor: `#${Math.floor(Math.random() * 16777215).toString(
16
)}`,
margin: 10,
display: 'inline-block',
}}
key={'item'+index}
>
{item}
</div>
)
})
}, [renderData])
return (
<div>
<div style={{ textAlign: 'center' }}>
<label>
<input
type="checkbox"
checked={isStartTransition}
onChange={(e) => {
setIsStartTransition(e.target.checked)
}}
/>
useTransition
</label>
<input
type="range"
value={rangeValue}
min={0}
max={10000}
style={{ width: 120 }}
onChange={handleChange}
/>
<span>进度条 {rangeValue}</span>
<hr />
</div>
{jsx}
</div>
)
}
17.2.14 useSyncExternalStore - 暂时了解
React18的beta版本将useMutableSource更新为了useSyncExternalStore,这个新的api将会对React的各种状态管理库产生非常大的影响,下面我来介绍useSyncExternalStore的用法和场景。
我们可以通过这个api自行设计一个redux + react-redux的数据方案:
1、设计store
首先我们要设计一个store,它必须有如下属性:
- currentState:当前状态
- subscribe:提供状态发生变化时的订阅能力
- getSnapshot: 获取当前状态
以及改变state的方法,这里参考redux,设计了dispatch、reducer
const store = {
currentState:{data:0},
listeners:[],
reducer(action){
switch(action.type) {
case 'ADD':
return {data:store.currentState.data+1}
default:
return store.state
}
},
subscribe(l){
store.listeners.push(l)
},
getSnapshot() {
return store.currentState
},
dispatch(action) {
store.currentState = store.reducer(action)
store.listeners.forEach(l=>l())
return action;
}
}
2、应用 store 同步组件状态
import React, { useSyncExternalStore } from 'react'
import store from './store'
export default function UseSyncExternalStoreDemo() {
const state = useSyncExternalStore(store.subscribe, () => store.getSnapshot().data);
return (
<div>
<div>count: {state}</div>
<div>
<button onClick={()=>store.dispatch({type:'ADD'})}>add+</button>
</div>
</div>
)
}
17.2.15 useInsertionEffect - 了解
useInsertionEffect 与useEffect相同,在所有DOM变更之前同步触发。在使用 useLayoutEffect 读取布局之前,使用这个函数将样式注入到DOM中。因为这个钩子的作用域是有限的,所以这个钩子不能访问 refs,也不能调度更新。
import React, { useInsertionEffect, useEffect, useLayoutEffect } from 'react'
export default function UseInsertionEffect() {
useInsertionEffect(() => {
console.log('useInsertionEffect') // 1
// const style = document.createElement('style')
// style.innerHTML = '.box { color: red }'
// document.head.appendChild(style)
})
useEffect(() => {
console.log('useEffect') // 3
})
useLayoutEffect(() => {
console.log('useLayoutEffect') // 2
})
return (
<div className="box">UseInsertionEffect</div>
)
}
useEffect - useLayoutEffect - useInsertionEffect
17.3 自定义hooks
以use开头的小驼峰式的函数