Hooks + Redux

185 阅读15分钟

hooks + redux

一、hooks

1.什么是hooks

hooks是React V16.8中 新增的功能,它不仅解决了函数组件this指向混乱的问题,还比之前更好地进行逻辑的复用了

它为函数组件添加了状态、生命周期。所以从16.8版本后,react逐渐从

class组件(提供状态)+函数组件(展示内容)

变成现在的

hooks(提供状态)+函数组件(展示内容)

有了hooks后,函数组件就不能称为无状态组件了,因为hooks为函数组件提供了状态

2.useState

// 函数式组件声明状态和设置状态

// 第一步,从react中导入useState
import React, { useState } from 'react'

const App = () => {
    // 第二步,useState()返回值是一个数组,第一个元素是状态,第二个元素是设置状态的方法
    // 声明状态就给useState()里传参就行了
    const [num, setNum] = useState(10)
    const [arr, setArr] = useState([1, 2, 3, 4])
    return (
        <>
            <h1>{num}</h1>
            <h1>{arr}</h1>
            {/* 第三步,设置状态直接在()里放新值,就不用采用之前的setState({})对象写法 */}
            <button onClick={() => setNum(num + 10)}>点击+10</button><br />
            <button onClick={() => setArr([...arr, 5])}>点击新增数字</button>
        </>
    )
}

export default App

tips:useState使用限制(react在底层为useState创建了一个链表,它记住了每个useState的顺序。当useState放在if语句或者是for语句里时,会造成顺序的混乱)


import React, { useState } from 'react'


const App = () => {
    // useState()三个使用限制
    const [arr, setArr] = useState([1, 2, 3, 4])
    // 1.不能写在for语句里
    // for(let i = 0;i<list.length;i++) {
    //     useState()
    // }

    // 2.不能写在if/while语句中
    // if(flag) useState()

    // 3.useState()只能写在非普通函数中
    // 3.1 非普通函数:①函数式组件(函数名以大写字母开头,返回一段jsx或者null)②自定义hooks(以use开头)
    return (
        <>
            <h1>{arr}</h1>
            <button onClick={() => setArr([...arr, 5])}>点击新增数字</button><br />
        </>
    )
}

export default App

3.useEffect


import React, { useEffect, useState } from 'react'

const App = () => {
    const [num, setNum] = useState(0)
    // useEffect本质是一个钩子函数(用于处理副作用,也就是与页面渲染无关的操作,例如 数据(Ajax)请求、手动修改 DOM、localStorage 操作等),对标class组件里的生命周期
    // useEffect可以拿到更新后的值,该 effect 会在每次组件更新(DOM更新)后执行
    useEffect(() => {
        document.title = num
    })

    return (
        <>
            <h1>{num}</h1>
            <button onClick={() => setNum(num + 1)}>点击+1</button>
        </>
    )
}

export default App

tips:react的组件本质公式是UI = f(state),所以不需要副作用的操作,这些多余的操作都放在useEffect去进行

4.useEffect的生命周期


import React, { useEffect, useState } from 'react'

const App = () => {
    const [num, setNum] = useState(0)
    // useEffect对标class组件的三个生命周期:componentDidMount(挂载),componentDidUpdate(更新),componentWillUnmount(卸载)
    // 也就是 挂载后,更新后,卸载后

    // 1. 挂载后,useEffect在第二个参数上写上一个[],这样只在挂载时执行一次,后续不再执行
    // useEffect(() => {
    //     console.log('挂载后');
    // }, [])


    // 2.挂载更新二合一,useEffect在第二个参数上写上 [依赖项],表示当依赖项变化时才出发更新,并且在挂载时也执行一次
    // 效果相当于vue的watch开启立即更新
    useEffect(() => {
        console.log('更新后');
    }, [num])

    return (
        <>
            <h1>{num}</h1>
            <button onClick={() => setNum(num + 1)}>点击+1</button>
        </>
    )
}

export default App

那么,useEffect卸载的生命周期该怎么写呢?它有点特殊,是和挂载后一起写


import React, { useEffect, useState } from 'react'

const App = () => {
    const [show, setShow] = useState(true)

    return (
        <>
            {show && <Child></Child>}
            <button onClick={() => setShow(!show)}>点击卸载组件</button>
        </>
    )
}

const Child = () => {
    // useEffect的挂载和卸载都写在同一个函数内,卸载操作的函数通过return返回,挂载的操作写在回调函数内
    useEffect(() => {
        // 挂载后开启页面改变事件监听
        const resizeFn = () => console.log('页面刷新了')
        window.addEventListener('resize', resizeFn)


        // retrun返回的函数表示卸载时执行的函数
        // 卸载时关闭监听事件
        return () => {
            window.removeEventListener('resize', resizeFn)
        }
    }, [])
    return <h1>这是Child组件</h1>
}

export default App

tips:使用useEffect的推荐写法是一个useEffect对应一个功能,这样会方便维护和修改

5.useEffect里使用async await注意事项

import React, { useEffect, useState } from 'react'
import request from './utils/request'


export default function App() {
    const [list, setList] = useState([])
    // async await不能在useEffect里使用,要在外部定义函数去使用再到这里调用
    useEffect(() => {
        getList()
    }, [])
    // 发送请求(记住async await要在这里使用)
    const getList = async () => {
        const res = await request.get('/v1_0/channels')
        setList(res.data.channels)
    }
    return (
        <>
            <ul>
                {list.map(item => <li key={item.id}>{item.name}</li>)}
            </ul>
        </>
    )
}

总结:useEffect不能直接使用async await,要将函数写在外面,然后在useEffect里调用

二、Redux

1.什么是Redux

redux是状态管理工具,对标vue的vuex。redux解决了react多个组件传值时,组件间数据流混乱的问题。它可以集中存储和管理应用的状态;无视组件之间的层级关系传值;单向数据流,结构清晰

2.Redux基本用法

// 导入redux
import { legacy_createStore as createStore } from 'redux'

// 初始化store对象(通过参数默认值来初始化state的数据)
// 接受两个参数,第一个参数是state,第二个参数是action
const store = createStore((state = { name: 'zs', age: 18 }, action) => {
    // 判断type是什么类型进行不同dispatch的操作
    if (action.type === 'add') {
        return { ...state, age: state.age + 8 }
    }
    if (action.type === 'des') {
        return { ...state, age: state.age - 4 }
    }
    return state
})

// 调用store的dispatch方法时,要传入一个对象,对象里要具有type属性(唯一修改state数据的方法)
store.dispatch({ type: 'add' })
store.dispatch({ type: 'des' })

// subscribe可以用来跟踪state里的数据变化(放在subscribe之前的变化相当于未订阅,不会跟踪state的变化)
// subscribe的返回值是一个函数,用于取消跟踪
const unSubscribe = store.subscribe(() => console.log(store.getState()))

store.dispatch({ type: 'add' })

// 在这之后的数据变化不会被跟踪
unSubscribe()


store.dispatch({ type: 'add' })
store.dispatch({ type: 'add' })

优化写法


// 优化redux写法
import { legacy_createStore as createStore } from 'redux'

// 2.单独创建state初始值对象
const personState = {
    name: 'zs',
    age: 18
}

// 1.将初始化store里的函数单独创建( createStore(function) )
const personReducer = (state = personState, action) => {
    // 3.使用switch case替换if else
    switch (action.type) {
        case 'add': return { ...state, age: state.age + 8 }
        case 'des': return { ...state, age: state.age - 4 }
        default: return state
    }
}

// 正常初始化store对象,函数换成上方定义的函数
const store = createStore(personReducer)

export default store

总结:初始化一个redux分为四步

①导入redux

import { legacy_createStore as createStore } from 'redux'

② 初始化state对象

const initState = {}

③ 创建reducer函数

const personReducer = (state = initState, {type,payload}) => {
    switch (type) {
        case 'add': return { ...state, age: state.age + payload }
        default: return state
    }
}

④初始化store对象并导出

const store = createStore(personReducer)

export default store

3.Redux在react的基础应用

import { useEffect, useState } from 'react'
import store from './store'

export default function App() {
    const [show, setShow] = useState(true)
    return (
        <div>
            <h1>Redux基础案例</h1>
            <button onClick={() => setShow(!show)}>点击切换卸载挂载</button>
            {show ? <Son /> : null}
        </div>
    )
}

function Son() {
    // 1.先将state里的count作为useState的count的初始值
    const [count, setCount] = useState(store.getState().count)

    // 3.因为redux不会更新视图,react引起视图变化只有setCount。
    // 所以挂载后使用subscribe去监听state的count值变化,count发生变化就将新值赋值给setCount
    useEffect(() => {
        const unCount = store.subscribe(() => {
            setCount(store.getState().count)
        })
        // 4. 卸载后取消监听
        return () => {
            unCount()
        }
    }, [])

    return (
        <div>
            <h2>子组件</h2>
            <h2>count:{count}</h2>
            {/* 2.点击时调用store里的dispatch修改state的count值 */}
            <button onClick={() => store.dispatch({ type: 'add' })}>+8</button>
            <button onClick={() => store.dispatch({ type: 'des' })}>-4</button>
        </div>
    )
}

使用react-redux可以简化写法

import { useState } from 'react'
// 第二步,将useDispatch,useSelector方法从react-redux中导出
import { useDispatch, useSelector } from 'react-redux'
import addCount from './store/action/count'

export default function App() {
    const [show, setShow] = useState(true)
    return (
        <div>
            <h1>Redux基础案例</h1>
            <button onClick={() => setShow(!show)}>点击切换卸载挂载</button>
            {show ? <Son /> : null}
        </div>
    )
}

function Son() {
    // 初始化变量使用useSelector(),参数是回调函数,里面传的是state的值
    const count = useSelector((state) => state.count)
    // 初始化方法使用useDispatch,直接调用即可
    const dispatch = useDispatch()
    return (
        <div>
            <h2>子组件</h2>
            <h2>count:{count}</h2>
            {/* 在action里可以传其他的参数来更加灵活的实现功能 */}
            {/* 建议是新建一个文件夹维护这个方法,这个函数返回一个带有type字段的对象 */}
            <button onClick={() => dispatch(addCount(8))}>+8</button>
            <button onClick={() => dispatch(addCount(-4))}>-4</button>
        </div>
    )
}

总结:react-redux简化了useState的使用,不用像之前一样先绑定变量,再去使用store.subscribe监听变量的变化,然后修改变量引起视图更新。而是通过新的useSelector实现数据驱动视图,并且通过useDispatch简化了dispatch函数的调用

4.Redux的工作化拆分模块

当我们页面的需求变得很复杂时,所有页面的业务处理全部写在store中就会显得很臃肿,也就是在写屎山(笑)。所以模块化开发就变得很重要,规范自己的写法就可以让代码看上去清晰了然。

①首先将store进行拆分,拆分成action和reducer两个模块

1658836681814.png

store/index文件

import { applyMiddleware, legacy_createStore as createStore } from "redux";
import { composeWithDevTools } from "redux-devtools-extension";
import thunk from "redux-thunk";
// 所有模块的reducer函数的合并函数
import rootReducer from "./reducer";

// ceateStore(合并的reducer函数,composeWithDevTools(applyMiddleware(中间件1,...)))
// 工具包应用,应用包中间件
const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(thunk)))
export default store

②每个模块都action函数都分开存放,标识符全部设置为常量通过actionType保存(方便修改和使用)

action/actionType.js文件

export const CHANNEL_SAVE_LIST = 'channel/saveList'
export const CHANNEL_UPDATE_ID = 'channel/updateId'
export const NEWS_SAVE_NEWSLIST = 'news/saveNewsList'

③每个模块的reducer函数也分开保存,设置一个index.js文件来合并所有模块的reducer函数

reducer/index.js

import { combineReducers } from "redux";
import channelReducer from "./channelReducer";
import newsRecucer from "./newsReducer";

// combineReducers用于合并reducer函数,返回值为一个函数
// 语法: combineReducers({ 模块名:该模块的reducer函数 })

const rootReducer = combineReducers({
    channel: channelReducer,
    news: newsRecucer
})

export default rootReducer

三、补充知识

1.useRef的使用

import React, { useEffect, useRef, useState } from 'react'

export default function App() {
  const [flag, setFlag] = useState(true)

  return (<>
    <div>{flag && <Child></Child>}</div>
    <button onClick={() => setFlag(!flag)}>卸载组件</button>
  </>
  )
}

const Child = () => {
  // 通过useRef创建ref对象
  const timeRef = useRef()
  useEffect(() => {
  // ref不仅可以获取dom元素和组件,还可以当作全局变量使用
    timeRef.current = setInterval(() => {
      console.log(1);
    }, 100)
  }, [])
  useEffect(() => {
    return () => {
      clearInterval(timeRef.current)
    }
  }, [])
  return <>
    <h1>子组件</h1>
  </>
}

总结:useRef和之前的createRef相比,虽然功能大致一样,但是写法简化了许多。

2.useHistory和useLocation

import React from 'react'
import { BrowserRouter as Router, Route, Switch, useHistory, useLocation } from 'react-router-dom'
export default class App extends React.Component {
  render() {
    return (
      <>
        <Router>
          <div className='router'>
            <Switch>
              <Route path="/l1" component={Luyou1}></Route>
              <Route path="/l2" component={Luyou2}></Route>
            </Switch>
          </div>
          <NotLuyou></NotLuyou>
        </Router>

      </>
    )
  }
}

function NotLuyou(props) {
  // useHistory可以让没有配置路径的组件拥有history属性
  const his = useHistory()
  console.log('没有配置路径', his);
  const loc = useLocation()
  console.log('没有配置路径', loc);
  return <h1>非路由组件</h1>;
}

function Luyou1(props) {
  console.log(31, props.history.location);
  return <h1>路由组件</h1>;
}

function Luyou2() {
  return <h1>路由组件</h1>;
}

总结:useHistory和useLocation都可以让没有配置过路径的组件拥有history对象和location对象,比传统的需要配置路径才能在props属性上拥有history和location对象相比,显然前者功能更加强大

3.react解决样式冲突

①首先要将样式文件名改成xxx.module.scss(css或者less都行)

②在这个文件里,除了:global所包着的选择器,其他选择器都会被重命名,会给这个类名改成不重复的hash类名

// react写样式推荐写法:
// 根标签选择器让react的module去重命名,根标签里的子标签选择器不接受重命名(放在:global里)

.box {
  background-color: orange;
  :global {
    .size {
      font-size: 40px;
    }
    .size-big {
      font-size: 100px;
    }
  }
}

4.逻辑复用

自定义hooks(最推荐)

import React from 'react'
import { useState } from 'react';

export default function App() {
  return (
    <div>
      <Header />
    </div>
  )
}

// 先定义一个自定义hooks函数(use开头)
const useMouse = () => {
  const [mouse, setMouse] = useState({ x: 0, y: 0 })
  const handleMouseMove = (e) => {
    setMouse({ x: e.clientX, y: e.clientY })
  }
  return [mouse, handleMouseMove]
}


function Header() {
  // 像之前使用useState一样,导入自定义hooks使用
  const [mouse, handleMouseMove] = useMouse()
  return (
    <div onMouseMove={handleMouseMove}>
      <h1>
        x: {mouse.x} - y: {mouse.y}
      </h1>
    </div>
  );
}

tips:自定义hooks只能使用在其他hooks和函数式组件中

hoc(高阶组件)复用逻辑

import React from 'react'

export default function App() {
  return (
    <div>
      <hr />
      {/*最后通过标签形式渲染*/} 
      <LogMain />
      <hr />
    </div>
  )
}

// hoc本质是一个函数,返回的是一个class类组件,类组件里render返回的是所传进来的组件
const logMouse = (Child) => {
    // 返回class组件,逻辑在里面完成
  return class Wrap extends React.Component {
    state = {
      mouse: { x: 0, y: 0 }
    }
    handleMouseMove = (e) => {
      this.setState({ mouse: { x: e.clientX, y: e.clientY } })
    }
    render() {
      // 将逻辑和状态通过父传子传给通过参数接收的组件
      const { mouse } = this.state
      return <Child mouse={mouse} handleMouseMove={this.handleMouseMove}></Child>
    }
  }
}



const Main = ({ mouse, handleMouseMove }) => {
  return (
    <div onMouseMove={handleMouseMove}>
      <h1>
        x:{mouse.x} - y:{mouse.y}
      </h1>
    </div>
  );
}

// 因为返回的是一个组件,用变量去接收,最后通过标签形式渲染
const LogMain = logMouse(Main)

renderProps逻辑复用(最不推荐)

import React from 'react'
import { useState } from 'react';

export default function App() {
  return (
    <div>
      {/* 直接在属性上写jsx,在render上写的函数会在类组件里被调用 */}     
      <LogMouse render={(mouse, handleMouseMove) => {
        return <>
          <div onMouseMove={handleMouseMove}>
            <h1>
              x: {mouse.x} - y: {mouse.y}
            </h1>
          </div>
        </>
      }} />
    </div>
  )
}

// renderProps其实就是创建一个类组件,通过render调用传给props属性里的方法(我调用我自己属性上的方法)
class LogMouse extends React.Component {
  // 正常在类组件里写逻辑
  state = {
    mouse: { x: 0, y: 0 }
  }
  handleMouseMove = (e) => {
    this.setState({ mouse: { x: e.clientX, y: e.clientY } })
  }
  render() {
    const { mouse } = this.state
    // render返回不是传统的jsx,而是取调用props上自定义的render方法,并将状态和逻辑通过参数传给props上的render函数
    return this.props.render(mouse, this.handleMouseMove)
  }
}

tips:现在最新的react主流是通过自定义hooks去实现逻辑的复用,后续的两种都是之前没有出自定义hooks时类组件复用逻辑的方式

四、ts+react补充

1.props结合ts在react的使用

import React, { Component } from 'react';

export default class App extends Component {
  render() {
    return (
      <>
        <div>App</div>
        <Child msg="zs" count={123}></Child>
      </>
    );
  }
}

// 定义接口去描述类型(类似vue的props的对象写法)
interface IProps {
  msg: string;
  count?: number;
}

// 要在ts里使用props要先定义描述props对象的接口,再去传值
const Child = ({ msg, count }: IProps) => {
  return <h1>Child</h1>;
};

2.ref结合ts

import React, { useRef } from 'react';

const App = () => {
  // 创建ref对象要结合泛型一起使用
  const iptRef = useRef<HTMLInputElement>(null);
  return (
    <>
      <input ref={iptRef} type="text" />
      <button onClick={() => iptRef.current?.focus()}>获取焦点</button>
    </>
  );
};

export default App;

五、其他补充

1.websocket

websocket本质其实就是一种通讯协议,它具有双向通信,长链接的特点。建立连接时,和http不同,websocket要附加一些身份认证信息,连接成功后双方就可以自由通信,,并且这个连接会持续存在直到某一方主动断开连接。主要用于聊天场景webpack-dev-serve(热更新)

而http也是一种通讯协议,但它是单向通信,通信完立刻断开

2.react的鉴权路由

react的鉴权路由就是vue的路由守卫

// 鉴权路由
<Route path="/My/edit"
   render={(data: any) => {
// 当token存在时才能访问
   if (IsHaveAuth()) {
       // 透传data
       return <ProfileEdit {...data} />;
      }
// token不存在时重定向到登录页
   return <Redirect to="/login" />;
 }}

原理就是通过renderProps,按照自定义的方式渲染组件

renderProps笔者前文有进行描述,此处就不多赘述

// 鉴权路由的封装

// 1.定义接口
interface IAuthRoute extends RouteProps {
  path: string;
}

// component: Component表示重命名(因为组件名要大写开头)
export function AuthRoute({path,component: Component,...restProps: IAuthRoute) 
{
  // 如果不存在component组件,则不渲染
  if (!Component) return null;
  return (
    <Route
      // 透传props
      {...restProps}
      path={path}
      render={(data: any) => {
        if (IsHaveAuth()) {
          // 透传data
          return <Component {...data} />;
        }
        return <Redirect to="/login" />;
      }}
    />
  );
}

总结:封装组件其实最重要的是套皮+透传,先去定义一个组件实现封装单个路由的功能,再通过props传参和透传进行逻辑复用

3.响应拦截器前端处理三种状态

// 响应拦截器
request.interceptors.response.use(
  function (response) {
    return response.data;
  },
  function (error) {
    // 1. 网络中断
    if (!error.response) {
      Toast.show({ content: '网络连接中断' });
    }
    // 2. 401(token过期)
    if (error.response.status === 401) {
      Toast.show({ content: '登录失效,请重新登录' });
      // 跳转到登录页
      history.push('/login');
      // 这里加return就不用再写个else来实现非401错误
      return Promise.reject(error);
    }
    // 3. 非401错误
    Toast.show({ content: error.response.data.message });
    return Promise.reject(error);
  }
);

4.无感刷新

无感刷新其实就是用户token过期时,前端进行的一种处理。主要的原理就是当用户token过期时,此时后台返回两个token(用于刷新的token一般有效时长都会比token长),另一个token就是用来刷新的refresh_token。前端利用refresh_token去请求新的token,然后替换掉旧的token,再发起上一次失败的请求,就完成了无感刷新

    // 拿出refresh_token
    const { refresh_token } = getAuth();

// 1 有token无refresh_token
    if (!refresh_token) {
      Toast.show({ content: '登录失效,请重新登录' });
      // 跳转到登录页
      history.push('/login');
      return Promise.reject(error);
    }


// 2 有token也有refresh_token(无感刷新)
    try {
      // 发送拿新的refresh_token的请求(注意,这里不能用封装的request,因为请求头自动携带了token)
      const res = await axios({
        method: 'put',
        url: baseURL + '/v1_0/authorizations',
        headers: { Authorization: 'Bearer ' + refresh_token },
      });
      // 拿到返回来的新token
      const newToken = res.data.data.token;
      // 替换之前的token
      setAuth({ token: newToken, refresh_token });
      // 发起上次失败的请求并返回结果
      return request(error.config);
    } catch (err) {
// 3 token和refresh_token都过期,就让用户重新登录(利用try catch捕获这个错误)
      Toast.show({ content: '登录失效,请重新登录' });
      // 跳转到登录页
      history.push('/login');
      return Promise.reject(error);
    }

6.函数式组件优化性能的三种方法

React.memo

import React from 'react';
import { useState } from 'react';

const Test = () => {
  const [count, setCount] = useState(0);
  const [msg, setsetMsg] = useState('');
  console.log('App更新了');
  return (
    <>
      <div onClick={() => setCount(count + 1)}>改变数字</div>
      <div onClick={() => setsetMsg(msg + 'A')}>改变字符串</div>
      <PureCount count={count} />
      <Msg msg={msg} />
    </>
  );
};

const Count = ({ count }: any) => {
  console.log('count更新了');
  return (
    <>
      <div>{count}</div>
    </>
  );
};
const Msg = ({ msg }: any) => {
  console.log('msg更新了');
  return (
    <>
      <div>{msg}</div>
    </>
  );
};
// 将组件作为参数,返回值作为标签使用
const PureCount = React.memo(Count);
export default Test;

React.memo类似类组件的PureComponent,都可以用来阻止无用更新

useMemo

让我们先看看这段代码

import { useState } from 'react';

const Test = () => {
  const [count, setCount] = useState(0);
  const [msg, setsetMsg] = useState('');
  const doubleCountFn = () => {
    console.log('我被触发了,开始计算');
    return count * 2;
  };
  const doubleCount = doubleCountFn();
  return (
    <>
      <div onClick={() => setCount(count + 1)}>改变数字</div>
      // 当改变字符串时,doubleCountFn也会被触发
      <div onClick={() => setsetMsg(msg + 'A')}>改变字符串</div>
      <h2>{doubleCount}</h2>
      <Count count={count} />
      <Msg msg={msg} />
    </>
  );
};

const Count = ({ count }: any) => {
  return (
    <>
      <div>{count}</div>
    </>
  );
};
const Msg = ({ msg }: any) => {
  return (
    <>
      <div>{msg}</div>
    </>
  );
};

export default Test;

我们不难发现,我们点击修改msg时也会触发doubleCountFn这个函数,这并不是我们想要的结果。

useMemo就可以避免这种情况的发生,它类似vue中的计算属性,具有缓存功能,只有影响计算结果的变量改变时采取计算。无关变量的改变不会去计算

import { useMemo, useState } from 'react';

const Test = () => {
  const [count, setCount] = useState(0);
  const [msg, setsetMsg] = useState('');
  // 类似useEffect更新写法,useMemo接收两个参数,第一个是回调函数,第二个是依赖项
  const doubleCount = useMemo(() => {
    console.log('我被触发了,开始计算');
    return count * 2;
  }, [count]);
  return (
    <>
      <div onClick={() => setCount(count + 1)}>改变数字</div>
      <div onClick={() => setsetMsg(msg + 'A')}>改变字符串</div>
      <h2>{doubleCount}</h2>
      <Count count={count} />
      <Msg msg={msg} />
    </>
  );
};

const Count = ({ count }: any) => {
  return (
    <>
      <div>{count}</div>
    </>
  );
};
const Msg = ({ msg }: any) => {
  return (
    <>
      <div>{msg}</div>
    </>
  );
};

export default Test;

这时改变msg的值就不会触发console.log的打印了

useCallback

useCallback主要用于避免函数被重复定义,让我们看看下面的代码

import { useEffect, useState } from 'react';

const Test = () => {
  const [count, setCount] = useState(0);
  const handleCount = (num: number) => {
    setCount(count + num);
  };
  return (
    <>
      <Count count={count} AddFn={handleCount} />
    </>
  );
};

const Count = ({ count, AddFn }: any) => {
  useEffect(() => {
    console.log('AddFn更新了');
  }, [AddFn]);

  return (
    <>
      <button onClick={() => AddFn(count + 2)}>点击修改count</button>
      <div>{count}</div>
    </>
  );
};

export default Test;

我们发现,当修改count的值时,useEffect会被一直触发。这是因为每次修改count,组件都会被刷新,然后handleCount又重新声明,传给AddFn。地址发生改变,useEffect就会被触发。

使用useCallback就可以避免这种情况,它的语法和useMemo相同:useCallback(回调函数,[依赖项])

import { useCallback, useEffect, useState } from 'react';

const Test = () => {
  const [count, setCount] = useState(0);
  const handleCount = useCallback((num: number) => {
    // 注意,此处不能直接使用count + 1,会产生闭包
    setCount(num + 1);
   // 此处不加依赖项
  }, []);

  return (
    <>
      <Count count={count} AddFn={handleCount} />
    </>
  );
};

const Count = ({ count, AddFn }: any) => {
  useEffect(() => {
    console.log('AddFn更新了');
  }, [AddFn]);

  return (
    <>
      <button onClick={() => AddFn(count + 2)}>点击修改count</button>
      <div>{count}</div>
    </>
  );
};

export default Test;

和useMemo不同之处在于,useCallback最好不要加依赖项,否则又会重新声明了