Hook 是 React 16.8 的新增特性,它可以让我们在不编写class的情况下使用state以及其他的React特性(比如生命周期)
hooks:
Hook的使用场景:
// 快捷键 => rpce
import React, { PureComponent } from 'react';
export class CountClass extends PureComponent {
constructor(props) {
super(props);
this.state = {
counter: 0
};
}
counterChange(num) {
this.setState({
counter: this.state.counter + num
});
}
render() {
// 快捷键 => dob
const { counter } = this.state;
return (
<>
当前记数 : {counter}
>
);
}
}
export default CountClass;
// 快捷键盘 => rmc
import { memo, useState } from 'react';
const App = memo(() => {
const [counter, setCounter] = useState(0);
return (
<>
当前计数 : {counter}
>
);
});
export default App;
State Hook => 用来创建state
useState来自react,需要从react中导入,它是一个hook
State Hook的API就是 useState :
// 1. 直接传入
const [count, setCount] = useState(0);
setCount(100);
const [position, setPosition] = useState({ x: 0, y: 0 });
setPosition({ x: 100, y: 100 });
// 2. 传入方法 => 方法会立即执行
const [name, setName] = useState(() => {
return 'hello';
});
Effect Hook => 用来创建生命周期
Effect Hook 可以完成一些类似于class中生命周期的功能
useEffect的解析:
页面的title总是显示counter的数字
// 快捷键 => rpce
import React, { PureComponent } from 'react';
export class CountClass extends PureComponent {
constructor(props) {
super(props);
this.state = {
counter: 66
};
}
componentDidMount() {
document.title = this.state.counter;
}
// 1. 使用这个生命周期函数的时候
componentDidUpdate() {
document.title = this.state.counter;
}
counterChange(num) {
this.setState(
{
counter: this.state.counter + num
},
() => {
// 2. 或者使用这个回调函数
document.title = this.state.counter;
}
);
}
render() {
// 快捷键 => dob
const { counter } = this.state;
return (
<>
当前记数 : {counter}
>
);
}
}
export default CountClass;
// 快捷键盘 => rmc
import { memo, useState, useEffect } from 'react';
const App = memo(() => {
const [counter, setCounter] = useState(999);
// 1. 直接赋值,因为每次都会重新渲染,所以每次都会重新赋值 => 不推荐
// document.title = counter;
// 2. 使用useEffect,当前传入的回调函数会在组件被渲染完成后,自动执行
useEffect(() => {
// 事件坚听、定时器、网络请求、订阅消息、手动修改DOM、修改ref等等
document.title = counter;
});
return (
<>
当前计数 : {counter}
>
);
});
export default App;
在class组件中,某些副作用的代码,需要在componentWillUnmount中进行清除
在useEffect中 :
useEffect(() => {
// 1. 事件坚听、定时器、网络请求、订阅消息、手动修改DOM、修改ref等等 => 第一次只执行这里,
const unsubcribe = sotre.subscribe(()=>{
})
function foo(){}
eventBus.on('xxx',foo)
// 2. 返回一个函数,这个函数会在组件要重新渲染或者被销毁的时候自动执行 => 之后都是先执行这里,再执行上面的
return ()=>{
unsubcribe()
eventBus.off('xxx',foo)
}
});
一个函数式组件中,可以存在多个useEffect
React 将按照 effect 声明的顺序依次调用组件中的每一个 effect
// 1. 修改标题
useEffect(() => {
document.title = counter;
});
// 2. 监听事件总线
useEffect(() => {
function foo() {}
eventBus.on('xxx', foo);
return () => {
eventBus.off('xxx', foo);
};
});
// 3. 监听redux中的数据变化
useEffect(() => {
const unsubcribe = sotre.subscribe(() => {});
return () => {
unsubcribe();
};
});
默认情况下,useEffect的回调函数会在每次渲染时都重新执行,但是这会导致两个问题
useEffect实际上有两个参数
受count影响的Effect => 会重新执行
// 快捷键盘 => rmc
import { memo, useState, useEffect } from 'react';
const App = memo(() => {
const [counter, setCounter] = useState(999);
const [isShow, setIsShow] = useState(true);
// 监听counter的变化
useEffect(() => {
document.title = counter;
console.log('监听counter的变化');
}, [counter]);
// 监听isShow的变化
useEffect(() => {
console.log('监听isShow的变化');
}, [isShow]);
// 监听事件总线
useEffect(() => {
console.log('事件总线');
}, []);
// 监听redux中的数据变化
useEffect(() => {
console.log('订阅数据');
}, []);
// 发起网络请求
useEffect(() => {
console.log('发起网络请求');
}, []);
return (
<>
当前计数 : {counter}
>
);
});
export default App;
在之前的开发中,要在组件中使用共享的Context有两种方式:
Context Hook允许通过Hook来直接获取某个Context的值
当Context传入的值更改时,会触发子组件的重新渲染
import { createContext } from 'react';
const counterContext = createContext();
const themeContext = createContext();
export { counterContext, themeContext };
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
import { counterContext, themeContext } from './context';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
);
import React, { memo, useContext } from 'react';
// 1. 导入context
import { CounterContext, ThemeContext } from './context';
const App = memo(() => {
// 2. 使用context
const count = useContext(CounterContext);
const theme = useContext(ThemeContext);
return (
<>
App
count: {count.counter}
theme: {theme.color}
>
);
});
export default App;
useReducer不是redux的某个替代品
useReducer仅仅是useState的一种替代方案:
import React, { memo, useState } from 'react';
const App = memo(() => {
const [count, setCount] = useState(0);
return (
<>
当前计数: {count}
>
);
});
export default App;
import React, { memo, useReducer } from 'react';
// 1. 定义 reducer 函数
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;
}
}
const App = memo(() => {
// 2. 使用 useReducer 函数,传入 reducer 函数和初始值,得到 state 和 dispatch 函数
const [state, dispatch] = useReducer(reducer, { counter: 0, friends: [], user: {} });
return (
<>
{/* 3. 使用 state 中的数据, 通过 state.属性名 来获取 */}
当前计数: {state.counter}
{/* 4. 使用 dispatch 函数,传入 action 对象,来触发 reducer 函数,从而改变 state */}
>
);
});
export default App;
useCallback实际的目的是为了进行性能的优化 => 返回值是优化后的函数
如何进行性能的优化 :
import React, { memo, useState } from 'react';
const App = memo(() => {
const [count, setCount] = useState(0);
return (
<>
当前计数: {count}
{/* 每次点击+1,都会重新渲染App组件,导致子组件也会重新渲染,该箭头函数也会重新创建,浪费性能 */}
>
);
});
export default App;
import React, { memo, useState, useCallback } from 'react';
const App = memo(() => {
const [count, setCount] = useState(0);
// useCallback 用于缓存函数,但是不会缓存函数中的变量,也就是传入的函数依然会重新创建,所以这样写是没用的
// 和原来的写法一样,没有任何优化
const handleBtnClick = useCallback((e) => {
setCount(count + 1);
});
return (
<>
当前计数: {count}
>
);
});
export default App;
import React, { memo, useState, useCallback } from 'react';
const App = memo(() => {
const [count, setCount] = useState(0);
// 小心闭包陷阱 => 当不传依赖的时候,函数只会创建一次。依赖项中的 count 一直是初始值 0,所以每次点击按钮,count 都为 1,而不是累加 => 没有创建新的函数
// const handleBtnClick = useCallback(function (e) {
// setCount(count + 1);
// }, []);
// 传入第二个参数,依赖项数组,当依赖项发生变化时,才会重新创建函数,否则复用之前的函数,避免了闭包陷阱
const handleBtnClick = useCallback(function (e) {
setCount(count + 1);
}, [count]);
return (
<>
当前计数: {count}
>
);
});
export default App;
上方做法依然没有进行优化,当count改变的时候,依然会创建新的函数
和普通的没有区别
当把函数从父组件传递给子组件,父组件有其他数据更改时,该优化才能生效
import React, { memo, useState, useCallback } from 'react';
const Home = memo((props) => {
console.log('Home组件被调用');
// 3. 子组件通过props接收父组件传递过来的函数
const { handleBtnClick } = props;
return (
<>
Home组件
{/* 4. 子组件通过props接收父组件传递过来的函数,并且调用 */}
{/* 假如有100个组件 => 全都会重新渲染 */}
>
);
});
const App = memo(() => {
const [count, setCount] = useState(0);
const [name, setName] = useState('jack');
// 1. 使用普通方法定义的函数
const handleBtnClick = (e) => setCount(count + 1);
return (
<>
当前计数: {count}
当前名称: {name}
{/* 2. 把函数传递给子组件,子组件通过props接收 */}
>
);
});
export default App;
import React, { memo, useState, useCallback } from 'react';
const Home = memo((props) => {
console.log('Home组件被调用');
// 3. 子组件通过props接收父组件传递过来的函数
const { handleBtnClick } = props;
return (
<>
Home组件
{/* 4. 子组件通过props接收父组件传递过来的函数,并且调用 */}
{/* 假如有100个组件 => 该组件不重新渲染,其下的组件也都不会,大大优化了 */}
>
);
});
const App = memo(() => {
const [count, setCount] = useState(0);
const [name, setName] = useState('jack');
// 1. 通过useCallback包裹函数,把函数传递给子组件
const handleBtnClick = useCallback(
function (e) {
setCount(count + 1);
},
[count]
);
return (
<>
当前计数: {count}
当前名称: {name}
{/* 2. 把函数传递给子组件,子组件通过props接收 */}
>
);
});
export default App;
当count发生改变的时候,也使用同一个函数
将count依赖移除掉 => 缺点 : 闭包陷阱
// 小心闭包陷阱 => 当不传依赖的时候,函数只会创建一次。依赖项中的 count 一直是初始值 0,所以每次点击按钮,count 都为 1,而不是累加 => 没有创建新的函数
const handleBtnClick = useCallback(function (e) {
setCount(count + 1);
}, []);
做法一 + 使用useRef => 特点 : 在组件多次渲染时,返回的是同一个值
import React, { memo, useState, useCallback, useRef } from 'react';
const Home = memo((props) => {
console.log('Home组件被调用,只会在组件第一次渲染时调用');
const { handleBtnClick } = props;
return (
<>
Home组件
>
);
});
const App = memo(() => {
const [count, setCount] = useState(0);
const [name, setName] = useState('jack');
// 1. useRef 保存的值,在组件重新渲染时,不会发生改变
const countRef = useRef();
// 2. 但是,可以通过修改 ref.current 的值,来达到保存数据的目的
countRef.current = count;
const handleBtnClick = useCallback(
(e) => {
setCount(countRef.current + 1);
},
// 3. 去除依赖项,让 useCallback 每次都返回同一个函数
[]
);
return (
<>
当前计数: {count}
当前名称: {name}
>
);
});
export default App;
useMemo实际的目的也是为了进行性能的优化 => 返回值是一个值
进行大量的计算操作,可以使用useMemo进行优化
只会被执行一次
import React, { memo, useMemo, useState } from 'react';
// 1. 写在函数组件外面的代码, 只会被执行一次,防止被多次创建
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);
// 2.不依赖任何的值, 进行计算 => 只会被执行一次
const result = useMemo(() => {
return calcNumTotal(50);
}, []);
return (
<>
计算结果: {result}
计数器: {count}
>
);
});
export default App;
每次count发生变化, 都会重新计算
import React, { memo, useMemo, useState } from 'react';
// 1. 写在函数组件外面的代码, 只会被执行一次,防止被多次创建
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);
// 2.依赖count, 进行计算 => 每次count发生变化, 都会重新计算
const result = useMemo(() => {
return calcNumTotal(count * 2);
}, [count]);
return (
<>
计算结果: {result}
计数器: {count}
>
);
});
export default App;
/**
* useMemo和useCallback的对比. useCallback返回的是一个函数, useMemo返回的是一个值
* 1. useCallback(fn, []) => fn
* 2. useMemo(() => fn, []) => fn()
* 这两种写法表示的意思是一样的
*/
function fn() {}
// 返回值是一个函数 => useCallback
const increment = useCallback(fn, []);
// 返回值是一个值 => useMemo
const increment2 = useMemo(() => fn, []);
如果对子组件传入相同内容的对象时,可以进行优化 => 因为每次执行函数,都会创建新对象,子组件就会重新渲染
如果传递给子组件的是个值的情况下,值不改变,子组件就不会重新渲染
import React, { memo, useMemo, useState } from 'react';
// 创建一个子组件
const HelloWorld = memo(function (props) {
console.log('HelloWorld被渲染~');
return Hello World
;
});
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 = useMemo(() => {
return calcNumTotal(50);
}, []);
// 1. 每次都会创建一个新的对象,导致子组件的props发生变化
// const info = { name: 'why', age: 18 }; // 会导致子组件的props发生变化,从而导致子组件的重新渲染
// 2. 使用useMemo对子组件渲染进行优化,只有当info发生变化的时候,才会重新创建新的对象,否则使用缓存的对象 => 优化子组件的渲染
const info = useMemo(() => ({ name: 'why', age: 18 }), []); // 不会导致子组件的props发生变化,从而不会导致子组件的重新渲染
return (
<>
计算结果: {result}
计数器: {count}
>
);
});
export default App;
useRef返回一个ref对象,返回的ref对象再组件的整个生命周期保持不变
ref是两种用法:
import React, { memo, useRef } from 'react';
const App = memo(() => {
// 1. useRef() 生成一个 ref 对象
const titleRef = useRef();
//3. 通过 ref 对象的 current 属性获取引用的元素
const getTitleRef = () => {
console.log(titleRef.current);
};
return (
<>
{/* 2. 将 ref 对象传递给需要引用的元素 */}
App111
>
);
});
export default App;
使用ref保存上一次的某一个值
import React, { memo, useCallback, useRef, useState } from 'react';
const App = memo(() => {
const [count, setCount] = useState(0);
const countRef = useRef(0);
countRef.current = count;
const handleClick = useCallback((e) => {
// 如果直接使用 count,会有闭包问题 !!! => 因为这里不依赖外界,所以只会创建一次函数, count 是在 handleClick 函数创建时就已经存在了,而不是每次渲染时才创建
// setCount(count + 1);
// 如果使用 countRef.current,就不会有闭包问题 !!! => 因为这里依赖外界,所以每次渲染都会创建新的函数, countRef.current 是在 handleClick 函数执行时才存在的
setCount(countRef.current + 1);
}, []);
return (
<>
App : {count}
>
);
});
export default App;
在父组件中,通过传递ref绑定子组件,获取的权限太高
可以通过useImperativeHandle,来约束外界的权限
import React, { memo, useRef, forwardRef, useImperativeHandle } from 'react';
// 1. 创建子组件 => 接收ref
const Child = memo(
forwardRef((props, ref) => {
// 2. 创建自己内部的ref
const childRef = useRef();
// 4. 暴露给父组件的方法 => 通过useImperativeHandle暴露给父组件
useImperativeHandle(ref, () => ({
focus: () => {
childRef.current.focus();
},
setValue(value) {
childRef.current.value = value;
}
}));
// 3. 将自己内部的ref绑定
return ;
})
);
const App = memo(() => {
// 5. 创建父组件的ref
const parentRef = useRef();
function handleDOM() {
// 6. 调用子组件暴露给父组件的方法 => parentRef.current拿到的是useImperativeHandle返回的对象
// console.log(parentRef.current)
parentRef.current.focus();
parentRef.current.setValue('哈哈哈');
// 无法直接操作子组件的value,因为没有暴露给父组件
// parentRef.current.value = ""
}
return (
<>
>
);
});
export default App;
useLayoutEffect看起来和useEffect非常的相似,事实上他们也只有一点区别而已
如果希望在某些操作发生之后再更新DOM,那么应该将这个操作放到useLayoutEffect
官方更推荐使用useEffect而不是useLayoutEffect
import React, { memo, useEffect, useLayoutEffect } from 'react';
const App = memo(() => {
useLayoutEffect(() => {
console.log('useLayoutEffect', '第二执行');
});
useEffect(() => {
console.log('useEffect', '第三执行');
});
console.log('App', '第一执行');
return App;
});
export default App;
自定义Hook本质上只是一种函数代码逻辑的抽取,严格意义上来说,它本身并不算React的特性
注意 : 名称需要用useXXX
所有的组件在创建和销毁时都进行打印
import { memo, useEffect, useState } from 'react';
// 1. 自定义hook,命名必须以use开头 => 生命周期
const useLife = (name) => {
useEffect(() => {
console.log(name + '被创建');
return () => {
console.log(name + '被销毁');
};
}, [name]);
};
const FirstComponent = memo(() => {
// 2. 使用自定义hook
useLife('firstComponent');
return firstComponent
;
});
const SecondComponent = memo(() => {
// 2. 使用自定义hook
useLife('secondComponent');
return secondComponent
;
});
const App = memo(() => {
// 2. 使用自定义hook
useLife('App');
const [isShow, setIsShow] = useState(true);
return (
<>
App
{isShow && }
{isShow && }
>
);
});
export default App;
创建context/index.js
import { createContext } from 'react';
const CounterContext = createContext();
const ThemeContext = createContext();
export { CounterContext, ThemeContext };
index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
import { CounterContext, ThemeContext } from './context';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
);
App.jsx
import { memo, useEffect, useContext } from 'react';
import { CounterContext, ThemeContext } from './context';
// 1. 自定义hook,命名必须以use开头 => context抽取共享
const useSelfContext = (name) => {
const counterContext = useContext(CounterContext);
const themeContext = useContext(ThemeContext);
return { counterContext, themeContext };
};
const FirstComponent = memo(() => {
// 2. 使用自定义hook
const { counterContext, themeContext } = useSelfContext();
return (
<>
firstComponent
counterContext {'=>'} {counterContext.counter}
themeContext {'=>'} {themeContext.theme}
>
);
});
const SecondComponent = memo(() => {
// 2. 使用自定义hook
const { counterContext, themeContext } = useSelfContext();
return (
<>
secondComponent
counterContext {'=>'} {counterContext.counter}
themeContext {'=>'} {themeContext.theme}
>
);
});
const App = memo(() => {
return (
<>
App
>
);
});
export default App;
import { memo, useEffect, useContext, useState } from 'react';
// 1. 自定义hook,命名必须以use开头 => 获取滚动位置
const useScrollPosition = () => {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(
() => {
function scrollFn() {
console.log('scroll', window.scrollY, window.scrollX);
setPosition({ x: window.scrollX, y: window.scrollY });
}
window.addEventListener('scroll', scrollFn);
return () => {
window.removeEventListener('scroll', scrollFn);
};
},
// 不依赖,只执行一次
[]
);
return position;
};
const FirstComponent = memo(() => {
// 2. 使用自定义hook
const position = useScrollPosition();
return (
<>
firstComponent : {position.x} - {position.y}
>
);
});
const SecondComponent = memo(() => {
return (
<>
secondComponent
>
);
});
const App = memo(() => {
return (
App
);
});
export default App;
import { memo, useEffect, useState } from 'react';
// 自定义hook,命名必须以use开头 => localStorage数据存储
const useLocalStorage = (key) => {
// 1. 通过 localStorage 获取数据,初始化 state
const [data, setData] = useState(() => {
return JSON.parse(localStorage.getItem(key)) || '';
});
// 2. 监听 localStorage 变化,更新 state
useEffect(() => {
localStorage.setItem(key, JSON.stringify(data));
}, [data, key]);
// 3. 返回 state 和更新 state 的函数
return [data, setData];
};
const SecondComponent = memo(() => {
// 1. 使用自定义 hook
const [name, setName] = useLocalStorage('name');
return (
<>
secondComponent : {name}
>
);
});
const App = memo(() => {
const [age, setAge] = useLocalStorage('age');
return (
App
age : {age}
);
});
export default App;
在之前的redux开发中,为了让组件和redux结合起来,我们使用了react-redux中的connect:
在Redux7.1开始,提供了Hook的方式,再也不需要编写connect以及对应的映射函数了
npm i react-redux @reduxjs/toolkit
store/modules/counter.js
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: {
counter: 66,
message: 'Hello Redux'
},
// The `reducers` field lets us define reducers and generate associated actions
reducers: {
// Use the PayloadAction type to declare the contents of `action.payload`
incrementAction(state, { payload }) {
state.counter += payload;
},
messageChangeAction(state, { payload }) {
state.message = payload;
}
}
});
export default counterSlice.reducer;
export const { incrementAction, messageChangeAction } = counterSlice.actions;
store/index.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './modules/counter';
const store = configureStore({
reducer: {
counter: counterReducer
// Add the generated reducer as a specific top-level slice
}
});
export default store;
index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
import { Provider } from 'react-redux';
import store from './store';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
);
import React, { memo } from 'react';
import { connect } from 'react-redux';
import { incrementAction } from './store/modules/counter';
const App = memo((props) => {
// 4. 接收状态和dispatch
const { counter, increment } = props;
return (
<>
App
counter: {counter}
>
);
});
// 1. 映射状态
const mapStateToProps = (state) => ({
counter: state.counter.counter
});
// 2. 映射dispatch
const mapDispatchToProps = (dispatch) => ({
increment(num) {
dispatch(incrementAction(num));
}
});
// 3. connect高阶组件
export default connect(mapStateToProps, mapDispatchToProps)(App);
import React, { memo } from 'react';
// 1. 导入
import { useSelector, useDispatch } from 'react-redux';
import { incrementAction } from './store/modules/counter';
const Home = memo((props) => {
const state = useSelector((state) => ({
message: state.counter.message
}));
console.log('Home render');
return (
<>
Home
message: {state.message}
>
);
});
const App = memo((props) => {
// 2. 获取store中的数据, 通过useSelector映射到组件中
const state = useSelector((state) => ({
counter: state.counter.counter
}));
// 3. 获取dispatch, 通过useDispatch映射到组件中
const dispatch = useDispatch();
const incrementHandle = (num) => {
// 4. 调用dispatch, 传入action
dispatch(incrementAction(num));
};
console.log('App render');
return (
<>
App
counter: {state.counter}
>
);
});
export default App;
子组件home使用了memo : memo包裹的组件,只有当props改变时,才会重新渲染
但是App组件更改了counter后,Home组件也跟着刷新了
Home组件更改message后,App也重新渲染了
原因 : 因为useSelector默认监听的是整个state,只要有一个地方改变了,全部重新渲染
在使用useSelector时,第二个参数传入浅层比较函数shallowEqual即可
// 导入入shallowEqual
import { useSelector, useDispatch, shallowEqual } from 'react-redux';
// ...
const state = useSelector(
(state) => ({
message: state.counter.message
}),
shallowEqual
);
// ...
const state = useSelector(
(state) => ({
counter: state.counter.counter
}),
shallowEqual
);