React初体验
需要引入三个包:react、react-dom、babel
- react :里面包含了react核心代码
- react-dom:里面是用来实现跨平台的代码
- babel:用来将 jsx 语法转换成 js 代码
- 使用babel,需要将 script 的 type 属性设置为 text/babel
- ReactDOM.createRoot 创建根节点,之后render后的内容包含在这个根中
- root.render 渲染 jsx 文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="root"></div>
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<script type="text/babel">
// react18 更新成这样渲染
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(<h2>hello react</h2>)
</script>
</body>
</html>
案例:更换文本 hello react
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="root"></div>
<!-- 引入 react -->
<script src="../lib/react.js"></script>
<!-- 引入 react-dom -->
<script src="../lib/react-dom.js"></script>
<!-- 引入 babel -->
<script src="../lib/babel.js"></script>
<script type="text/babel">
// 1. 将文本定义成变量
let message = 'hello react'
// 2. 定义函数
function btnClick() {
message = '123456'
rootRender()
}
const root = ReactDOM.createRoot(document.getElementById('root'))
function rootRender() {
root.render(
<div>
<h2>{message}</h2>
<button onClick={btnClick}>修改文本</button>
</div>
)
}
rootRender()
</script>
</body>
</html>
类组件
数据在哪里定义
参与界面更新的数据
不参与界面更新的数据
JSX的使用
书写规范
注释
- 可以使用 { /**/ } 来添加注释
JSX具体用法
JSX浅入变量作为子元素
- 情况一:当变量是Number、String、Array类型时,可以直接显示
- 情况二:当变量是null、undefined、boolean类型时,内容为空
- 情况三:Object对象不能作为子元素
- 可以插入表达式
JSX嵌入表达式
JSX绑定属性
- title属性
- 内联样式 style
- img 的 src 属性等
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="root"></div>
<!-- 引入 react -->
<script src="../lib/react.js"></script>
<!-- 引入 react-dom -->
<script src="../lib/react-dom.js"></script>
<!-- 引入 babel -->
<script src="../lib/babel.js"></script>
<script type="text/babel">
class App extends React.Component {
constructor() {
super()
this.state = {
message: 'Hello, World',
title: '哈哈哈',
imgUrl: 'https://pic35.photophoto.cn/20150511/0034034892281415_b.jpg',
href: 'https://www.baidu.com',
isActive: false,
objStyle: { color: 'red', fontSize: '13px' }
}
}
render() {
const { message, title, imgUrl, href, isActive, objStyle } = this.state
return (
<div>
{/* 绑定 title 属性 */}
<h2 title={title}>h2</h2>
{/* 绑定 src 属性 */}
<img src={imgUrl} alt="" />
{/* 绑定 href 属性 */}
<a href={href}>百度一下</a>
{/* 绑定 class 属性 */}
<h2 className={isActive ? 'abc' : ''}>123</h2>
{/* 绑定 style 属性 */}
<h2 style={objStyle}>456</h2>
</div>
)
}
}
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(<App />)
</script>
</body>
</html>
JSX 转换过程
- 所有的JSX都会转成 React.createElement() 函数
JSX首先通过 Babel 进行转换,每遇到一个标签,转换成 React.createElement('div', {class, id},[]) 进行函数调用,最后变成一个树结构(虚拟DOM)
React组件化开发
生命周期
案例:
import React from 'react'
export default class HelloWorld extends React.Component {
// 1. 构造方法
constructor() {
console.log('hello world constructor')
super()
this.state = {
message: 'hello world'
}
}
changeTest() {
this.setState({ message: '你好呀' })
}
// 2. render 方法
render() {
console.log('hello world render')
const { message } = this.state
return (
<div>
<div>{message}</div>
<button onClick={e => this.changeTest()}>修改文本</button>
</div>
)
}
// 3. 组件被挂载到 DOM
componentDidMount() {
console.log('helloworld Mount')
}
// 4. 组件 DOM 更新完成 updated
componentDidUpdate() {
console.log('helloworld Updated')
}
// 5. 销毁组件
componentWillUnmount() {
console.log('helloworld Unmount')
}
}
三个阶段
主要分为下面三个阶段:
生命周期函数
主要分为下面几个函数:
- componentDidMount :组件挂载到DOM上,触发回调
- componentDidUpdate:组件发生更新时,触发回调
- componentWillUnmount:组件即将被移除时,触发回调
- shouldComponentUpdate: 要不要更新DOM (返false不执行) ,性能优化
- getSnapshotBeforeUpdate:在DOM更新之前,提前保存数据
- getDerivedStateFromProps:state的值在任何时候都依赖于props使用;该方法返回一个对象来更新state
Context 上下文
用来给子组件传递数据
使用步骤:
- 创建一个 Context(一般抽取成js文件)
- 用<ThemeContext.Provider value={{需要传递的数据}}> </ThemeContext.Provider> 包裹需要传递数据的子组件
- 在子组件中也需要引入 Context,然后用 组件名.contextType = Context名,即可用 this.context 取到传递的值
context.js
import React from 'react';
const ThemeContext = React.createContext();
export default ThemeContext
App.jsx
import React, { Component } from 'react'
import Home from './Home'
import ThemeContext from './context/theme-context'
export class App extends Component {
render() {
return (
<div>
<ThemeContext.Provider value={{ color: 'red', size: '30' }}>
<Home />
</ThemeContext.Provider>
</div>
)
}
}
export default App
Home.jsx
import React, { Component } from 'react'
import ThemeContext from './context/theme-context'
export class Home extends Component {
render() {
console.log(this.context)
return <div>Home</div>
}
}
Home.contextType = ThemeContext
export default Home
组件传值
props-Types 参数类型限制
- 还可以使用 Typescript、Flow 来进行限制
- propsTypes 参数类型限制
- defaultProps 添加默认值
import React, { Component } from 'react'
import PropTypes from 'prop-types'
class MainBanner extends Component {
render() {
const { banners } = this.props
return <div>{banners}</div>
}
}
// 参数限制
MainBanner.propsTypes = {
// 类型为数组,必传项
banners: PropTypes.array.isRequired
}
MainBanner.defaultProps = {
banners: []
}
export default MainBanner
父传子:
- 父组件通过
- 子组件通过 props 接收,this.props
父组件:banner.jsx
import React, { Component } from 'react'
import MainBanner from './MainBanner'
import MainProductList from './MainProductList'
export default class Main extends Component {
constructor() {
super()
this.state = {
banners: ['歌曲', 'MV', '歌单'],
productList: ['商品1', '商品2', '商品3']
}
}
render() {
const { banners, productList } = this.state
return (
<div>
Main
<MainBanner banners={banners} />
<MainProductList productList={productList} />
</div>
)
}
}
子组件:MainBanner.jsx
import React, { Component } from 'react'
export default class MainBanner extends Component {
render() {
const { banners } = this.props
return <div>{banners}</div>
}
}
子传父:
- 也是通过prop的原理,只不过是传递了一个函数
父组件App.jsx:
import React, { Component } from 'react'
import AddCounter from './AddCounter'
export class App extends Component {
constructor() {
super()
this.state = {
counter: 100
}
}
changeCount(count) {
this.setState({ counter: this.state.counter + count })
}
render() {
const { counter } = this.state
return (
<div>
<h2>当前计数:{counter}</h2>
<AddCounter addClick={count => this.changeCount(count)} />
</div>
)
}
}
export default App
子组件AddCount.jsx:
import React, { Component } from 'react'
export class AddCounter extends Component {
addCount(val) {
console.log(val)
this.props.addClick(val)
}
render() {
return (
<div>
<button onClick={e => this.addCount(1)}>+1</button>
<button onClick={e => this.addCount(5)}>+5</button>
<button onClick={e => this.addCount(10)}>+10</button>
</div>
)
}
}
export default AddCounter
案例:tabs切换
父组件:App.jsx
import React, { Component } from 'react'
import Tabs from './Tabs'
export class App extends Component {
constructor() {
super()
this.state = {
titles: ['流行', '新款', '精选'],
currentIndex: 0
}
}
changeParentIndex(index) {
this.setState({ currentIndex: index })
}
render() {
const { titles, currentIndex } = this.state
return (
<div className="app">
<Tabs titles={titles} changeParentIndex={index => this.changeParentIndex(index)} />
<h1>{titles[currentIndex]}</h1>
</div>
)
}
}
export default App
子组件:Tabs.jsx
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import './style.css'
export class Tabs extends Component {
constructor() {
super()
this.state = {
currentIndex: 0
}
}
changeIndex(index) {
this.props.changeParentIndex(index)
this.setState({ currentIndex: index })
}
render() {
const { titles } = this.props
const { currentIndex } = this.state
return (
<div className="tabs">
{titles.map((item, index) => {
return (
<div className={`item ${index === currentIndex ? 'active' : ''}`} key={item} onClick={e => this.changeIndex(index)}>
<span className="text">{item}</span>
</div>
)
})}
</div>
)
}
}
Tabs.propTypes = {
titles: PropTypes.array.isRequired
}
Tabs.defaultProps = {
titles: []
}
export default Tabs
插槽 Slot
使用children实现
App.jsx
import React, { Component } from 'react'
import NavBar from './nav-bar/NavBar'
export class App extends Component {
render() {
return (
<div>
<NavBar>
<button>按钮</button>
<h2>标题</h2>
<i>123</i>
</NavBar>
</div>
)
}
}
export default App
NavBar.jsx
import React, { Component } from 'react'
import './style.css'
export class NavBar extends Component {
render() {
const { children } = this.props
return (
<div className="nav-bar">
<div className="left">{children[0]}</div>
<div className="center">{children[1]}</div>
<div className="right">{children[2]}</div>
</div>
)
}
}
export default NavBar
使用props实现
App.jsx
import React, { Component } from 'react'
import NavBar from './nav-bar/NavBar'
import NavBar2 from './nav-bar2/NavBar'
export class App extends Component {
render() {
return (
<div>
{/* 使用 children 方式 */}
<NavBar>
<button>按钮</button>
<h2>标题</h2>
<i>123</i>
</NavBar>
{/* 使用 props 的方式 */}
<NavBar2
leftSlot={<button>按钮2</button>}
centerSlot={231}
rightSlot={321} />
</div>
)
}
}
export default App
Nav-Bar.jsx
import React, { Component } from 'react'
import './style.css'
export class NavBar2 extends Component {
render() {
const { leftSlot, centerSlot, rightSlot } = this.props
return (
<div className="nav-bar">
<div className="left">{leftSlot}</div>
<div className="center">{centerSlot}</div>
<div className="right">{rightSlot}</div>
</div>
)
}
}
export default NavBar2
根据组件定义方式分类
函数组件
类组件
根据组件内部是否有状态需要维护分类
无状态组件
有状态组件
根据组件不同职责分类
展示型组件
容器型组件
EventBus
- event.on 监听事件
- event.emit 发出事件
- event.off 取消事件
-
App.jsx
import React, { Component } from 'react'
import Home from './Home'
import eventBus from './utils/event-bus'
export class App extends Component {
bannerPrevClick() {
console.log('app 监听 bannerPrev')
}
componentDidMount() {
eventBus.on('bannerPrev', this.bannerPrevClick)
}
componentWillUnmount() {
eventBus.off('bannerPrev', this.bannerPrevClick )
}
render() {
return (
<div>
<h2>App Component</h2>
<Home />
</div>
)
}
}
export default App
HomeBanner.jsx
import React, { Component } from 'react'
import eventBus from './utils/event-bus'
export class HomeBanner extends Component {
prevClick() {
console.log('上一个')
eventBus.emit('bannerPrev', 10)
}
nextClick() {
console.log('下一个')
eventBus.emit('bannerNext', 9)
}
render() {
return (
<div>
<h2>HomeBanner</h2>
<button onClick={e => this.prevClick()}>上一个</button>
<button onClick={e => this.nextClick()}>下一个</button>
</div>
)
}
}
export default HomeBanner
获取 DOM
获取类组件
- 通过 this.refs.ref名称
- 通过 let titleRef = createRef()函数,然后通过 ref = {this.titleRef()} 获取
export class App extends PureComponent {
constructor() {
super()
this.state = {}
this.hwRef = createRef()
}
getComponent() {
console.log(this.hwRef.current.test())
}
render() {
return (
<div>
<button onClick={e => this.getComponent()}> 获取组件实例 </button>
<HelloWorld ref={this.hwRef} />
</div>
)
}
}
获取函数式组件
- 通过 React.forwardRef() 方法
import React, { PureComponent, createRef, forwardRef } from 'react'
const HelloWorld = forwardRef(function (props, ref) {
return (
<div>
<h1 ref={ref}>hello world</h1>
<p>哈哈哈</p>
</div>
)
})
React 更新机制
React 渲染流程
React 更新流程
- React 在 props 或 state 发生改变时,会调用 React 的 render 方法,创建一棵不同的树
- 如果按照下述过程进行比较的话,时间复杂度为O(n³)
- 此时需要用改进的diff算法,只进行同层级比较
diff 算法
- 同层节点之间相互比较,不会跨节点比较
- 不同类型的节点,产生不同的树结构
- 开发中,通过key指定那些节点在不同渲染下保持稳定
性能优化
render 函数
SCU 优化
- 可以使用 shouldComponentUpdate(nextProps, nextState) 生命周期函数,在里面对数据进行对比,可以提高性能,因为有一些组件在其他文本被改变的时候,他也重新渲染了
PureComponent 组件(用于类组件)
- 继承PureComponent组件,可以省去多个判断条件,是React本身提供的
import React, { Component, PureComponent } from 'react'
export class App extends PureComponent {
constructor() {
super()
this.state = {
message: 'hello',
counter: 0
}
}
render() {
const { message, counter } = this.state
return (
<div>
<h2>
App-{message}-{counter}
</h2>
</div>
)
}
}
export default App
Memo (函数组件)
- 使用 memo 方法将组件进行包裹
import { memo } from 'react'
const Profile = memo(function (props) {
console.log('profile render')
return <h2>Profile: {props.message}</h2>
})
export default Profile
数据不可变的力量
扩展运算符
shallowEqual() 方法
高阶组件
- 英文名:Higher-Order Components,简称 HOC
- 接收一个组件作为他的参数,返回一个新的组件作为返回值
- 相当于对组件进行了一层拦截操作,对组件进行增强
- 如果大量使用HOC,会产生很多嵌套,不利于调试
- HOC可以劫持props,在不遵守约定的情况下可能造成冲突
import React, { PureComponent } from 'react'
// 1. 定义一个高阶函数
function hoc(Cpn) {
// 1. 定义类组件
class NewCpn extends PureComponent {
render() {
return <Cpn />
}
}
return NewCpn
}
class HelloWorld extends PureComponent {
render() {
return (
<div>
<h1>hello world</h1>
</div>
)
}
}
const HelloWorldHOC = hoc(HelloWorld)
export class App extends PureComponent {
render() {
return (
<div>
<HelloWorldHOC />
</div>
)
}
}
export default App
案例 1
App.jsx
import React, { PureComponent } from 'react'
import enhancedUserInfo from './enhanced_props'
const userInfo = {
name: 'codeWhy',
level: 99
}
const Home = enhancedUserInfo(function (props) {
return <h1>home:{props.name}</h1>
})
const Profile = enhancedUserInfo(function (props) {
return <h1>Profile</h1>
})
const HelloFriend = enhancedUserInfo(function (props) {
return <h1>hellofriend</h1>
})
export class App extends PureComponent {
render() {
return (
<div>
<Home />
<Profile />
<HelloFriend />
</div>
)
}
}
export default App
enhanced_props.js
import { PureComponent } from "react"
function enhancedUserInfo(OriginComponent) {
class NewComponent extends PureComponent {
constructor() {
super()
this.state = {
userInfo: {
name: 'codeWhy',
level: 99
}
}
}
render() {
return <OriginComponent {...this.props} {...this.state.userInfo} />
}
}
return NewComponent
}
export default enhancedUserInfo
案例2:使用context+hoc增强
App.jsx
import React, { PureComponent } from 'react'
import ThemeContext from './context/theme_context'
import Product from './pages/Product'
export class App extends PureComponent {
render() {
return (
<div>
<ThemeContext.Provider value={{ color: 'red', size: 30 }}>
<Product />
</ThemeContext.Provider>
</div>
)
}
}
export default App
Product.jsx
import React, { PureComponent } from 'react'
import withTheme from '../hoc/with_theme'
export class Product extends PureComponent {
render() {
const { color, size } = this.props
return (
<div>
product: {color} {size}
</div>
)
}
}
export default withTheme(Product)
with_theme.js
import ThemeContext from "../context/theme_context"
function withTheme(OriginComponent) {
return (props) => {
// function NewComponent(props) {
return (
<ThemeContext.Consumer>
{
value => {
return <OriginComponent {...value} {...props} />
}
}
</ThemeContext.Consumer>
)
}
}
export default withTheme
案例3:登录鉴权
App.jsx
login_auth.js
function loginAuth(OriginComponent) {
return props => {
// 从 localStorage 获取 token
const token = localStorage.getItem("token")
if (token) {
return <OriginComponent {...props} />
}
else {
return <h2>请先登录</h2>
}
}
}
export default loginAuth
cart.jsx
import React, { PureComponent } from 'react'
import loginAuth from '../hoc/login_auth'
export class Cart extends PureComponent {
render() {
return <div>Cart</div>
}
}
export default loginAuth(Cart)
Portals 组件
- 可以使组件不挂载到root中,让他挂载到指定元素上
- 需要提供两个参数
- 第一个:可以是任何可渲染的React子元素
- 第二个:是一个DOM元素
- import {createPortals} from 'react-dom'
Fragment 组件
- 可以用Fragment包裹标签,不会生成额外标签
import React, { Fragment, PureComponent } from 'react'
export class App extends PureComponent {
render() {
return (
<Fragment>
<h2>我是App标题</h2>
<p>我是App内容,哈哈哈哈</p>
</Fragment>
)
}
}
export default App
StrictMode 严格模式
- 会检测不安全的生命周期
- 检测过时 Api
- 检查意外的副作用
- 组件的constructor会调用两次,这是故意进行的操作,让你查看你写的一些逻辑代码被调用多次是否有副作用
- 在生产环境不会调用两次
- 检测过时的 Context Api
React 过渡动画
react-transition-group库
- Transition:该组件和平台无关,不一定要结合css
- CSSTransition:在前端开发中,一般用这个完成过渡动画
- SwitchTransition:两个组件显示和隐藏切换
- TransitionGroup:将多个动画组件包裹,一般用于列表中元素动画
CSSTransition
React中如何编写CSS
内联样式
普通CSS
- 会导致CSS影响全局,需要配置 css modules
- 因为 React 脚手架内置了 css modules,将 .css/.less/.scss 等文件修改成 .module.css/.module.less
- 通过在App.jsx引入 App.module.css 文件
- 但是这种方法不太方便
App.jsx
import React, { PureComponent } from 'react'
import appStyle from './App.module.css'
export class App extends PureComponent {
render() {
return (
<div>
<h2 className={appStyle.title}>我是标题</h2>
</div>
)
}
}
export default App
App.module.css
.title {
font-size: 50px;
}
CSS in JS
styled-components
使用步骤
- npm install styled-components
- 新建 style.js 文件
- 编写 style.js
import styled from 'styled-components'
import { primaryColor } from './style/variables'
export const AppWrapper = styled.div`
.section {
border: 1px solid red;
}
.footer {
border: 1px solid orange
}
`
// 可以接收外部传入的 props
export const SectionWrapper = styled.div`
.title {
font-size: ${props => props.size}px;
color: ${props => props.color};
}
.content {
color: ${primaryColor}
}
`
- 在App.jsx中引入
import React, { PureComponent } from 'react'
import { AppWrapper } from './style'
export class App extends PureComponent {
render() {
return (
<AppWrapper className="app">
<div className="section">
<div className="title">我是标题</div>
<div className="content">我是内容</div>
</div>
<div className="footer">
<p>免责声明</p>
</div>
</AppWrapper>
)
}
}
export default App
高级特性
- 通过 jsx 传入属性
// 可以接收外部传入的 props
export const SectionWrapper = styled.div.attrs(props => {return { tColor: props:color || 'blue'} })`
.title {
font-size: ${props => props.size}px;
color: ${props => props.color};
}
`
- 当 js 里面的属性没有传入使,使用 attrs({ color:‘blue' }) 方法
classnames
Redux状态管理库
Redux 的三大原则
- 单一数据源
- 可以让整个应用程序的state变得方便维护、追踪、修改
- State 是只读的
- 唯一修改State的方法一定是派发action,不要再其他地方试图修改state
- 这样确保了View或网络请求不能直接修改state,只能通过action来改
- 这样可以保证所有的修改都能被集中化处理,并且按照严格的顺序来执行,不需要担心 race condition(竟态)问题
- 使用纯函数来修改
结构划分
如果把所有逻辑代码写在一起,那么当redux变得复杂时代码就非常难以维护,所以将redux进行拆解
Store
Action
Reducer
案例 1
reducer
import { ADD_NUMBER } from './constant'
const initialState = {
counter: 1
}
function reducer(state = initialState, action) {
switch (action.type) {
case ADD_NUMBER:
return { ...state, counter: state.counter + action.num }
default:
return state
}
}
export default reducer;
constant
export const ADD_NUMBER = "ADD_NUMBER"
action_creators
import { ADD_NUMBER } from "./constant"
export function addNumberAction(number) {
return { type: ADD_NUMBER, number }
}
index
import { createStore } from 'redux'
import reducer from './reducer'
export const store = createStore(reducer)
Home.jsx
import React, { PureComponent } from 'react'
import { store } from './store'
import { addNumberAction } from './store/action_creator'
export class Home extends PureComponent {
constructor() {
super()
this.state = {
counter: store.getState().counter
}
}
componentDidMount() {
store.subscribe(() => {
const state = store.getState()
this.setState({ counter: state.counter })
})
}
componentWillUnmount() {}
addNumber(number) {
store.dispatch(addNumberAction(number))
}
render() {
const { counter } = store.getState()
return (
<div>
<h2>Home:</h2>
<h2>当前计数:{counter}</h2>
<button onClick={e => this.addNumber(1)}>+1</button>
<button>+5</button>
</div>
)
}
}
export default Home
react-redux库
使用步骤(类组件中)
- 再 index.js 中将store全局引入,用Provider 包裹传递
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { Provider } from 'react-redux';
import { store } from './store';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
- 在组件内使用
import React, { PureComponent } from 'react'
import { connect } from 'react-redux'
export class about extends PureComponent {
calcNumber(num) {
this.props.addNumber(num)
}
render() {
return (
<div>
<button onClick={e => this.calcNumber(88, true)}> +5</button>
</div>
)
}
}
// 要将那些 store 里面的数据映射到 About 组件中
const mapStateToProps = state => ({
counter: state.counter
})
// 内部其实是再做: <About {...this.props} {...obj}/>
// 该函数会映射到 props 中
const mapDistchToProps = dispatch => ({
addNumber(num) {
dispatch(this.addNumberAction(num))
},
subNumber() {}
})
// connext() 的返回值是一个高阶组件
export default connect(mapStateToProps, mapDistchToProps)(about)
案例:发送网络请求
Category.jsx
import React, { PureComponent } from 'react'
import axios from 'axios'
import { connect } from 'react-redux'
import { fetchHomeMultidataAction } from '../store/action_creator'
export class category extends PureComponent {
componentDidMount() {
this.props.fetchHomeMultidata()
}
render() {
return (
<div>
<h2>category page: </h2>
</div>
)
}
}
const mapDispatchToProps = dispatch => ({
fetchHomeMultidata() {
dispatch(fetchHomeMultidataAction())
}
})
export default connect(null, mapDispatchToProps)(category)
action.js
import { ADD_NUMBER, CHANGE_BANNERS, CHANGE_RECOMMENDS } from "./constant"
import axios from 'axios'
export function addNumberAction(number) {
return { type: ADD_NUMBER, number }
}
export const changeBannersAction = (banners) => ({
type: CHANGE_BANNERS, banners
})
export const changeRecommendsAction = (recommends) => ({
type: CHANGE_RECOMMENDS, recommends
})
export const fetchHomeMultidataAction = () => {
return function (dispatch, getState) {
// 网络请求
axios.get('http://123.207.32.32:8000/home/multidata').then(res => {
const banners = res.data.data.banner.list
const recommends = res.data.data.recommend.list
dispatch(changeBannersAction(banners))
dispatch(changeRecommendsAction(recommends))
})
}
}
index.js
import { createStore, applyMiddleware } from 'redux'
import reducer from './reducer'
import thunk from 'redux-thunk'
export const store = createStore(reducer, applyMiddleware(thunk))
redux Tookkit
- 安装库:npm install @reduxjs/toolkit react-redux
- 核心API
案例:减法
Profile.jsx
import React, { PureComponent } from 'react'
import { connect } from 'react-redux'
import { subNumber } from '../store/features/counter'
export class Profile extends PureComponent {
subNumber(num) {
this.props.subNumber(num)
}
render() {
const { counter } = this.props
return (
<div>
<h2>Profile Counter: {counter}</h2>
<button onClick={e => this.subNumber(5)}>-5</button>
<button onClick={e => this.subNumber(8)}>-8</button>
</div>
)
}
}
const mapStateToProps = state => ({
counter: state.counter.counter
})
const mapDistchToProps = dispatch => ({
subNumber(num) {
dispatch(subNumber(num))
}
})
export default connect(mapStateToProps, mapDistchToProps)(Profile)
counter.js
import { createSlice } from '@reduxjs/toolkit'
const counterSlice = createSlice({
name: "counter",
initialState: {
counter: 888
},
reducers: {
// 相当于每一个 case 语句,获取 action,修改 state
subNumber(state, { payload }) {
state.counter = state.counter - payload
}
}
})
export const { addNumber, subNumber } = counterSlice.actions
export default counterSlice.reducer
index.js
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './features/counter'
const store = configureStore({
reducer: {
counter: counterReducer
}
})
export default store
React-Route路由
React-Route的基本使用
路由映射配置
- Routes:包裹所有的 Route,在其中匹配一个路由
- Router5.x 使用的是 Switch
- Route:用于路径匹配
- path 属性:用于设置匹配到的路径
- element属性:设置匹配到路径后,渲染的组件
- Router5.x 使用的是 component 属性
- exact:精确匹配,只有精准匹配到完全一致的路径,才会渲染对应的组件
- Router6.x 不再支持该属性
<div className="content">
<Routes>
<Route path="/home" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</div>
路由配置和跳转
Link和NavLink
- 通常路径的跳转是使用Link组件,最终会渲染成 a 元素
- NavLink 是在 Link 的基础之上加了一些样式属性
- to 属性:Link 中最重要的属性,用于设置跳转路径
- replace属性:不保留历史记录
<div className="nav">
<Link to="/home">首页</Link>
<Link to="/about">关于</Link>
</div>
NavLink
- style :需要传入一个函数
- style={({ isActive }) => ({ color: isActive ? 'red' : '' })}
- className:传入函数,函数接受一个对象,包括 isActive 属性
<NavLink to="/home" style={({ isActive }) => ({ color: isActive ? 'red' : '' })}>
首页
</NavLink>
<NavLink to="/about">关于</NavLink>
Navigate跳转标签
<div>
<h1>Login: page</h1>
{!isLogin ? <button onClick={e => this.login()}>登录</button> : <Navigate to="/home" />}
</div>
<Routes>
<Route path="/" element={<Navigate to="/home" />} />
<Route path="/home" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/login" element={<Login />} />
</Routes>
NotFound 配置
路由嵌套
App.js
<Routes>
<Route path="/" element={<Navigate to="/home" />} />
<Route path="/home" element={<Home />}>
<Route path="/home/recommend" element={<HomeRecommend />}></Route>
<Route path="/home/ranking" element={<HomeRanking />}></Route>
</Route>
<Route path="/about" element={<About />} />
<Route path="/login" element={<Login />} />
<Route path="*" element={<NotFound />} />
</Routes>
Home.jsx
import React, { PureComponent } from 'react'
import { Link, Outlet } from 'react-router-dom'
export class Home extends PureComponent {
render() {
return (
<div>
<h1>Home page</h1>
<div className="home-nav">
<Link to="/home/recommend">推荐</Link>
<Link to="/home/ranking">排行榜</Link>
</div>
<Outlet />
</div>
)
}
}
export default Home
HomeRecommend.jsx
import React, { PureComponent } from 'react'
export class HomeRecommend extends PureComponent {
render() {
return (
<div>
<h2>Banner</h2>
<h2>歌单列表</h2>
</div>
)
}
}
export default HomeRecommend
类组件使用 路由hook
- 使用高阶组件进行封装
import { useNavigate } from "react-router-dom"
// 高阶组件:函数
export function withRouter(WrapperComponent) {
return function (props) {
const navigate = useNavigate()
const router = { navigate }
return <WrapperComponent {...props} router={router} />
}
}
Router的两种模式
BrowserRouter
HashRouter
- 使用 hash 模式
Router 的参数传递
params 传递(/user/:id)
query 传递(/user?username=1&age=18)
- 使用 useLocation()
router 配置文件
router.js
import Home from '../pages/Home'
import About from '../pages/About'
import Login from '../pages/Login'
import NotFound from '../pages/NotFound'
import HomeRecommend from '../pages/HomeRecommend'
import HomeRanking from '../pages/HomeRanking'
import Category from '../pages/Category'
import HomeSongMenu from '../pages/HomeSongMenu'
import Order from '../pages/Order'
import Detail from '../pages/Detail'
import User from '../pages/User'
import { Navigate } from 'react-router-dom'
const router = [
{
path: "/",
element: <Navigate to="/home" />
},
{
path: "/home",
element: <Home />,
children: [
{
path: '/home',
element: <Navigate to="/home/recommend" />
},
{
path: '/home/recommend',
element: <HomeRecommend />
},
{
path: '/home/ranking',
element: <HomeRanking />
},
{
path: '/home/songmenu',
element: <HomeSongMenu />
},
]
},
{
path: '/about',
element: <About />
},
{
path: "/login",
element: <Login />
},
{
path: "/category",
element: <Category />
},
{
path: '/order',
element: <Order />
},
{
path: '/user',
element: <User />
},
{
path: '/detail',
element: <Detail />
},
{
path: '*',
element: <NotFound />
}
]
export default router
App.jsx
import React, { PureComponent } from 'react'
import { Routes, Route, Link, NavLink, Navigate, useRoutes } from 'react-router-dom'
import router from './router'
export function App() {
return (
<div>
<div className="app">
<div className="header">
<span>header</span>
<div className="nav">
<NavLink to="/home" style={({ isActive }) => ({ color: isActive ? 'red' : '' })}>
首页
</NavLink>
<NavLink to="/about">关于</NavLink>
<NavLink to="/login">登录</NavLink>
<Link to="/user?name=why&age=19">用户</Link>
<button onClick={e => this.navigateTo('/category')}>分类</button>
<span>订单</span>
</div>
</div>
<div className="content">
{useRoutes(router)}
</div>
<div className="footer">footer</div>
</div>
</div>
)
}
export default App
router 懒加载
Suspense 组件
- 配置页面 loading
root.render(
<HashRouter>
<Suspense fallback={<h3>loading</h3>}>
<App />
</Suspense>
</HashRouter>
);
Hooks
- 普通函数不能使用 Hook
- 再自定义 hook 中,需要用到 hook,必须要以 use 开头
- 为什么要以 use 开头,而不是 create 开头?
为什么需要 Hook
- Hook是 React 16.8 的新增特性,它可以让我们在不编写lass的情况下使用state以及其他的React特性(比如生命周期)
- 我们先来思考一下class组件相对于函数式组件有什么优势?比较常见的是下面的优势:
- class组件可以定义自己的state,用来保存组件自己内部的状态
- 函数式组件不可以,因为函数每次调用都会产生新的临时变量
- class组件有自己的生命周期,我们可以在对应的生命周期中完成自己的逻辑;
useState
- useState 来自于 react,需要从 react 导入,是一个 hook
- 参数:初始化值,不设置为 undefined
- 返回值:数组,包含两个元素
useEffect
- 可以完成类似于class中生命周期的功能
- 类似于网络请求、手动更新DOM、一些事件的监听,都可以用 useEffect
- 通过该 hook,可以告诉 React 需要再渲染后执行某些操作
- useEffect要求我们传入一个回调函数,当React执行完恒心DOM操作之后,就会回调
- 默认情况下,无论是第一次渲染,还是每次更新之后,都会执行该回调
- 为什么要在 effect 中返回一个函数?
- 这是effect 可选的清除机制。每个 effect 都可以返回一个清除函数
- 可以将添加和移除订阅的逻辑放在一起
- 都属于 effect 的一部分
- React 会在组件更新和卸载时执行清除操作
如何清除 Effect(副作用)
- 在 class 组件中,一般在 componentWillUnMount 中执行
- 在函数式组件中,通过在 useEffect() 函数的返回函数中来实现
- 用来代替 class 中的 componentWillUnMount 生命周期
useEffect(() => {
console.log('监听')
return () => {
console.log('取消 redux')
}
})
Effect 性能优化
- 某些代码我们只希望执行一次即可
- 另外,多次执行也会导致一定的性能问题
- 我们如何决定useEffect 在什么时候应该执行和不执行呢?
- useEffect有两个参数:
- 参数一:执行的回调函数
- 参数二:该 useEffect 在那些 state 发生变化时,才重新执行
- 如果一个函数不希望依赖任何的内容,就可以传入一个 空数组[]
- 如果受某些变量影响,例如:[message],那么当message发生变化,就会执行
useContext
- 避免了 class 组件中 context 的繁琐用法
- 当 context 发生变化,组件自动重新渲染
- 使用步骤如下:
- 首先在 index.js 中引入 context,使用 context 对 进行包裹
root.render(
<UserContext.Provider value={{ name: 'why', level: 99 }}>
<ThemeContext.Provider value={{ color: 'red', size: 30 }}>
<App />
</ThemeContext.Provider>
</UserContext.Provider>
);
- 然后在需要使用 context 的 jsx 中,引入 context 和 useConext(),拿到返回值直接使用
import React, { memo, useContext } from 'react'
import { UserContext, ThemeContext } from './context'
const App = memo(() => {
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 的代替方案
- 如果某种场景下,state 的处理逻辑比较复杂,可以通过 useReducer 进行拆分
import React, { memo, useReducer, 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 }
default:
return state
}
}
const App = memo(() => {
const [state, dispatch] = useReducer(reducer, { counter: 0 })
return (
<div>
<h2>当前计数:{state.counter}</h2>
<button onClick={e => dispatch({ type: 'increment' })}>+1</button>
<button onClick={e => dispatch({ type: 'decrement' })}>-1</button>
</div>
)
})
export default App
useCallback
- 用来实现性能优化
- 有闭包陷阱
- 如何进行性能优化呢?
- useCallback会返回一个函数的 memoized(记忆的)值
- 在依赖不变的情况下,多次定义的时候,返回的值是相同的
- 性能优化的点
- 当需要将一个函数传递给子组件时,最好使用useCallback进行优化,将优化后的函数传递给子组件
import React, { memo, useCallback, useMemo, useRef, useState } from 'react'
const HYIncrement = memo(props => {
const { increment } = props
return <button onClick={increment}>increment + 1</button>
})
const App = memo(() => {
const [count, setCount] = useState(0)
const [message, setMessage] = useState('hello')
// 闭包陷阱 useCallback
// 进一步优化,当count发生变化时,也使用同一个函数
// 做法一:将 count 从数组中移出,缺点是闭包陷阱
// 做法二:useRef,在组件多次渲染时,返回的值是同一个
const countRef = useRef()
countRef.current = count
const increment = useCallback(function foo() {
console.log('increment')
setCount(countRef.current + 1)
}, [])
// const increment = useCallback(
// function foo() {
// console.log('increment')
// setCount(count + 1)
// },
// [count]
// )
return (
<div>
<h2>计数:{count}</h2>
<button onClick={increment}>+1</button>
<HYIncrement increment={increment} />
<h2>message: {message}</h2>
<button onClick={e => setMessage(Math.random())}>修改message</button>
</div>
)
})
export default App
useMemo
- 对返回结果做优化,而 useCallback对返回函数做优化
import React, { memo, useMemo, useState } from 'react'
function calcNumTotal(num) {
console.log('被调哟')
let total = 0
for (let i = 0; i < num; i++) {
total += i
}
return total
}
const App = memo(() => {
const [count, setCount] = useState(0)
let result = useMemo(() => {
return calcNumTotal(count * 2)
}, [count])
return (
<div>
<h2>计算结果:{result}</h2>
<h2>计数器:{count}</h2>
<button onClick={e => setCount(count + 1)}>+1</button>
</div>
)
})
export default App
useRef
- 返回一个 ref 对象,返回的 ref 在组件的整个生命周期保持不变
- 可以解决闭包陷阱
import React, { memo, useCallback, useRef, useState } from 'react'
let obj = null
const App = memo(() => {
const [count, setCount] = useState(0)
const nameRef = useRef()
console.log(nameRef === obj)
obj = nameRef
// 通过 useRef 解决闭包陷阱
const increment = useCallback(() => {
setCount(count + 1)
}, [])
return (
<div>
<h2>Hello World : {count}</h2>
<button onClick={e => setCount(count + 1)}>+1</button>
</div>
)
})
export default App
useLayoutEffect
- 看起来和 useEffect 相似
- useEffect 会在渲染的内容更新到 DOM 上后执行,不会阻塞 DOM 更新
- useLayoutEffect 会在渲染的内容更新到 DOM 之前执行,会阻塞 DOM 更新
import React, { memo, useEffect, useLayoutEffect, useState } from 'react'
const App = memo(() => {
const [count, setCount] = useState(0)
useEffect(() => {
console.log('useEffect')
}, [])
useLayoutEffect(() => {
console.log('useLayoutEffect')
})
return (
<div>
<h2>count : {count}</h2>
<button onClick={e => setCount(count + 1)}>+1</button>
</div>
)
})
export default App
useImperativeHandle
- 用来限制子组件对父组件的操作权限
import React, { memo, useRef, forwardRef, useImperativeHandle } from 'react'
const HelloWorld = memo(
forwardRef((props, ref) => {
// 子组件对父组件传入的 ref 进行处理
useImperativeHandle(ref, () => {
return {
focus() {
console.log('focus')
ref.current.focus()
}
}
})
return <input type="text" ref={ref} />
})
)
const App = memo(() => {
const titleRef = useRef()
const inputRef = useRef()
function handleDom() {
inputRef.current.focus()
inputRef.current.value = ''
}
return (
<div>
<h2 ref={titleRef}>哈哈哈</h2>
<HelloWorld ref={inputRef} />
<button onClick={handleDom}>DOM操作</button>
</div>
)
})
export default App
useTransition
- 返回一个状态值表示过渡任务的等待状态,以及一个启动该过渡任务的函数
- 告诉 react 对于某部分任务的更新优先级较低,可以稍后进行更新
import React, { memo, useState, useTransition } from 'react'
import namesArray from './namesArray'
const App = memo(() => {
const [showNames, setShowNames] = useState(namesArray)
const [pending, setTransition] = useTransition()
function valueChangeHandle(event) {
setTransition(() => {
const keyword = event.target.value
const filterShowNames = showNames.filter(item => item.includes(keyword))
setShowNames(filterShowNames)
})
}
return (
<div>
<input type="text" onInput={valueChangeHandle} />
<h2>用户名列表:{pending && <span>正在加载</span>}</h2>
<ul>
{showNames.map((item, index) => {
return <li key={index}>{item}</li>
})}
</ul>
</div>
)
})
export default App
useDeferredValue
自定义 Hook
案例:useLogLife
function useLogLife(cName) {
useEffect(() => {
console.log(cName + '组件创建')
return () => {
console.log(cName + '组件销毁')
}
}, [])
}
案例:useUserToken
// useUserToken.js
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
// App.jsx
import React, { memo, useContext, useState } from 'react'
import { useUserToken } from './hooks'
const Home = memo(() => {
const [user, token] = useUserToken()
return <h1>Home Page - {token}</h1>
})
const About = memo(() => {
return <h1>About Page</h1>
})
const App = memo(() => {
const [isShow, setIsShow] = useState(true)
return (
<div>
<h1>App Root Component</h1>
<button onClick={e => setIsShow(!isShow)}>切换</button>
<Home />
<About />
</div>
)
})
export default App
// hooks/index.js
import useUserToken from "./useUserToken";
export { useUserToken }
案例:useScrollPosition
// useScrollPosition.js
import { useEffect, useState } from "react"
function useScrollPosition() {
const [scrollX, setScrollX] = useState(0)
const [scrollY, setScrollY] = useState(0)
useEffect(() => {
function handleScroll() {
setScrollX(window.scrollX)
setScrollY(window.scrollY)
console.log(window.scrollX, window.scrollY)
}
window.addEventListener('scroll', handleScroll)
return () => {
window.removeEventListener('scroll', handleScroll)
}
}, [])
return [scrollX, scrollY]
}
export { useScrollPosition };
// App.jsx
import React, { memo, useContext, useEffect, useState } from 'react'
import './style.css'
import { useScrollPosition } from './hooks/useScrollPosition'
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
案例:useLocalStorage
import { useEffect, useState } from "react"
function useLocalStorage(key) {
const [data, setData] = useState(JSON.parse(localStorage.getItem(key)))
useEffect(() => {
localStorage.setItem(key, JSON.stringify(data));
}, [data])
return [data, setData]
}
export default useLocalStorage
redux hooks
- 在之前的 redux 开发中,为了让组件和 redux 结合,使用了 react-redux 的 connect
- 但是这种方式必须使用高阶函数组合返回的高阶组件
- 必须编写:mapStateToProps 和 mapDispatchToProps 映射的函数
- 在 Redux7.1开始,提供了 hook 的方式,可以不需要再编写 connect 和对应的映射函数了
- useSelector :类似于 mapStateToProps
- 作用是将 state 映射到组件中
- 会默认比较返回的两个对象是否相等
- 参数一:将 state 映射到需要的数据中
- 参数二:可以进行比较来决定是否组件重新渲染 shallowEqual
- 使用 useSelector 相当于监听了整个 state,需要用shallowEqual
- useDispatch 的作用是获取 dispatch 函数
- useStore 获取当前的 store 对象
import React, { memo } from 'react'
import { useSelector, useDispatch, shallowEqual } from 'react-redux'
import { addNumberAction, subNumberAction } from './store/modules/counter'
const Home = memo(props => {
const { message } = useSelector(
state => ({
message: state.counterReducer.message
}),
shallowEqual
)
console.log('Home render')
return (
<div>
<h2>Home: {message}</h2>
</div>
)
})
const App = memo(props => {
// 1. 使用 useSelector 将数据映射到组件中
const { counter } = useSelector(state => ({
counter: state.counterReducer.counter
}))
// 2.
const dispatch = useDispatch()
function addNumberHandle(num, isAdd = true) {
if (isAdd) {
dispatch(addNumberAction(num))
} else {
dispatch(subNumberAction(num))
}
}
console.log('App render')
return (
<div>
<h2>当前计数:{counter}</h2>
<button onClick={e => addNumberHandle(1)}>+1</button>
<button onClick={e => addNumberHandle(-6, false)}>-6</button>
<hr />
<Home />
</div>
)
})
export default App
- 创建项目的方式:create-react-app
- 项目配置:
- 配置项目的 icon
- 配置项目标题
- 配置 jsconfig.json
- 通过 craco 配置别名和 less 文件
面试题
this 指向问题
因为 经过 babel 转换的代码,是使用严格模式,而在严格模式里面,this 为 undefined
- 可以利用 handleClick = () => {}
- 利用 bind(this)
- 利用 () => this.btnClick()
class App extends React.Component {
handleClick() {
console.log(this);// undefined
}
render() {
return (
<button onClick={this.btnClick.bind(this)}>按钮</button>
<button onClick={this.btnClick2}>按钮</button>
<button onClick={() => this.btnClick2()}>按钮</button>
)
}
}
this 的四种绑定规则:
- 默认绑定:独立执行 foo()
- 隐式绑定:被一个对象执行 obj.foo() -> obj
- 显式绑定:call / apply / bind -> foo.call
- new 绑定: new Foo() -> 创建一个新对象,将它赋值给 this
React 调用规则
当你在 React 中创建一个对象时,实际上是通过
React.createElement("button", {onClick: this.btnClick }}这种方式来创建对象,
然后将 onClick 也赋值过去,内部通过定义变量来接收,步骤为:
- const click = config.onClick
- click()
这种方法属于上述的默认绑定this,所以this指向window,但是在 babel 环境下,使用了严格模式,所以 this 为 undefined
为什么使用 setState 函数
- vue中是没有setState 函数来进行更新数据,而是他的底层会默认的将 template -> render() 进行转化( div -> h('div', {}, children)),vue有数据劫持
- react 里面没有 template 而是直接有render函数 (div -> React.createElement('div', {}, children),React中没有数据劫持,而是通过 setState 进行判断
- 如果上一个数据和当前一样,可以用 shouldComponentUpdate() 生命周期函数处理
- setState 函数 是从 Component 组件中继承而来
setState 详细使用
- setState 基本用法
- 原理:类似于创建一个新对象,传递了过去,是用 Object.assign(this.state, newState) 将两个对象合并
- this.setState({ message: 'aaa' })
- setState 可以传入一个回调函数
- 好处一:可以在回调函数中编写新的 state 的逻辑
- 好处二:当前的回调函数,会将之前的 state 和 props 传递进来
- this.setState(() => {return { message: '' }
- setState 在 React 的事件处理中,是异步的
为什么 setState 设计成异步的?
- React18之前是同步代码(使用setTimeout),React之后是异步
- 可以显著的提高性能
- 如果每次调用 setState 都进行一次更新,那么 render() 被重复调用,页面重新渲染,效率低下
- 最好的办法是获取到多个更新,之后进行批量更新
- 如果同步更新了 state,但是还没有执行 render() 函数,那么 state 和 props 不能保持同步
什么是闭包陷阱?
useCallback 和 useMemo
爱彼迎项目
难点
封装按钮滚动
- 记录索引:posIndex
- newIndex = posIndex + 1
- 根据 newIndex 索引获取到子元素 children[newIndex]
- newEl.offsetLeft
- 注意:设置定位
- 修改 scrollContentRef.current.style.transform = translate(xx)
- 设置最新索引
- 判断左边按钮是否显示
- 判断右边按钮是否显示
项目遇到的问题
useState 设置值
- 因为 useState 只有组件第一次被渲染时,才是有效的
- 当你后面从 infoData 里面取值然后赋值是,是无效的
- 解决方法:当这个值存在的时候,再渲染组件
// 从 props 获取数据
const { infoData } = props
// 定义内部的 state
const initialName = Object.keys(infoData.dest_list ?? {})[0] ?? ''
console.log(initialName)
const [name, setName] = useState(initialName)
const tabNames = infoData.dest_address?.map(item => item.name)
const tabClickHandle = useCallback((index, name) => {
setName(name)
}, [])
细节
防止页面一直刷新
外界传入的函数,最好用 useCallback包裹
- 可以防止多次定义