React18学习笔记

140 阅读10分钟

笔记指向课程:b站吴悠讲编程,如有错误欢迎指正~

1. 创建项目

  1. 终端 npx create-next-app 项目名
  2. cd 项目名 进入项目目录
  3. npm start 启动项目

1.1 一些介绍

两个重要文件

  1. index.js为入口文件,引入了两个库ReactReactDOM,通过ReactDOM.creatRoot创建一个react实例,通过render渲染根组件,React.StrictMode进行组件内部的功能审查

  2. React组件的两种创建方式:函数组件(主推)、类组件

2. JSX

即js和html语法写一起

2.1 基本用法

1) 函数组件return后面的小括号(),不换行可不写小括号

2)jsx只能返回一个根元素,如果必须设计多级,两方法

  • 再写个容器包住

  • 用空标签包住(渲染时不会产生多余标签)

    return(
    	<>
            <div>1</div>
            <div>2</div>
    	</>
    )
    

2.2 数据插值

2.2.1基础使用

插值可以使用的位置: 1.标签位置 2.标签内容

function App() {
  const divContent = '标签内容'
  const divTitle = '标签标题'
  return (
    <div title={divTitle}>{divContent}</div>
  )
}

image-20250815182437734.png

但是如果你写成<div title="{divTitle}">{divContent}</div> 给插值加上了括号,title就变成普通属性了

image-20250815182420968.png

2.2.2条件渲染

function App() {
  const divTitle = '标签标题'
  let divContent = null
  const flag = false
  if(flag){
    divContent = <span>flag为true</span>
  }else{
    divContent = <p>flag为false</p>
  }
  return (
    <div title={divTitle}>{divContent}</div>
  )
}

注意不要加引号写成divContent = '<span>flag为true</span>'

2.2.3列表渲染

import { Fragment } from 'react'

function App() {
  const list = [
    {id: 1,name:'小吴'},
    {id: 2,name:'小李'},
    {id: 3,name:'小花'},
  ]
  
  const listContent = list.map(item=>(
    <Fragment key={item.id}>
      <li>{item.name}</li>
      <li>---------</li>
    </Fragment>
  ))
  return (
    <ul>{listContent}</ul>
  )
}

由于只能有一个根目录,所以map下两个li元素可以写成如下形式

const listContent = list.map(item=>(
    <>
      <li key={item.id}>{item.name}</li>
      <li>---------</li>
    </>
))

如果<li>---------</li>也需要遍历展示呢?

我们知道 空标签<></>是不会被渲染成结构的,因此key定义在空标签上是不会生效的

这时可使用 React提供的Fragment标签,同时要记得在前面import引用该标签

2.2.4事件处理

jsx属性大多使用驼峰命名

function App() {
  function handleClick(e){
    console.log('点击按钮',e)
  }
  return (
    <button onClick={handleClick}>按钮</button>
  )
}

2.2.5状态处理

字符串的状态更改
import { useState } from 'react'
function App() {
  const [content,setContent] = useState('标签的默认内容')

  function handleClick(e){
    setContent('新内容')
  }
  return (
    <>
      <div>{content}</div>
      <button onClick={handleClick}>按钮</button>
    </>
  )
}

useState是react提供的一个函数,解构出来的content代表本次渲染的内容,setContent是一个用来修改这个状态内容的函数,能实现类似于vue响应式的效果

对象的状态更改
import { useState } from 'react'
function App() {
  const [data,setData] = useState({
    title:'默认标题',
    content:'默认内容'
  })

  function handleClick(){
    setData({
      ...data,
      title: '新标题'
    })
  }
  return (
    <>
      <div title={data.title}>{data.content}</div>
      <button onClick={handleClick}>按钮</button>
    </>
  )
}

对于对象的状态操作,其实和字符串差不多,唯一一个要注意的点是,在setData修改内容的时候,不能只写修改内容的操作,即

function handleClick(){
    setData({
      title: '新标题'
    })
}

这样会造成其他属性内容的丢失,在修改属性操作前加上...data拓展操作,即把其他不变属性也添加进来,便不会造成内容丢失

数组的状态更改
function App() {

  const [data,setData] = useState([
    {id: 1,name:'小吴'},
    {id: 2,name:'小李'},
    {id: 3,name:'小花'},
  ])

  const listData = data.map(item=>(
    <li key={item.id}>{item.name}</li>
  ))

  function handleClick(){
    setData(data.filter(item=>item.id!==2))
  }
  return (
    <>
      <ul>{listData}</ul>
      <button onClick={handleClick}>按钮</button>
    </>
  )
}

3.组件通信和插槽

3.1一些基础语法

1)看起来像html属性的功能,在react中称为为DOM组件设置Props

举例1

import image from 'XXX'
<img
	src={image}
	alt=""
/>

对于src第一种方法是 import(如示例),第二种方法是使用在线地址

举例2

vue中的class属性 对应 react中的className

className=''

举例3

style属性采用键值对写法

style={{
   width:100, // width: '100px'
   height:100
}}

如果要加单位,要添加引号

但是更推荐下面的写法

const imgStyle={
    xxx样式内容
}
style={imgStyle}

举例4

在react使用css中带-的属性,例如background-color,一般要写成驼峰形式,如backgroundColor

3.2jsx展开语法和props

import image from './logo.svg';

function App() {
  const imgData = {
    className:'small',
    style:{
      width: 100,
      height: 100,
      backgroundColor: 'grey'
    },
    src:image
  }
  return (
    <div>
      <img 
        alt=""
        {...imgData}
      />
    </div>
  )
}

在一个标签里设置太多属性,书写繁琐

react便提供了一种展开语法,将属性集中写在一个对象中,再应用在标签内(其中{...imgData}中的大括号是jsx单独的功能支持)

注:jsx的展开操作不是es6中的展开运算符

jsx的展开操作必须写在一个容器中,例如<img/>,但是es6的展开运算符是可以单独暴露出来的,如const a = {...b}

但是如果是要打印console.log()imgData中的内容,不能写成console.log(...imgData),要写成console.log({...imgData})

3.3react自定义组件

function Article(props) {
  return (
    <div>
      <h2>{props.title}</h2>
      <p>{props.content}</p>
      <p>状态{active?'显示中':'已隐藏'}</p>
    </div>
  )
}

export default function App(){
  return(
    <>  
      <Article
        title='标题1'
        content='内容1'
      />
      <Article
        title='标题2'
        content='内容2'
      />
      <Article
        title='标题3'
        content='内容3'
      />
    </>
  )
};

也可以不写成props点xxx的形式,需要使用到es6的解构

function Article({title,content,active}) {
  return (
    <div>
      <h2>{title}</h2>
      <p>{content}</p>
      <p>状态{active?'显示中':'已隐藏'}</p>
    </div>
  )
}

3.3.1react组件使用props

父传子

传递的值不可修改

function Detail({content,active}){
  return(
    <>
      <p>{content}</p>
      <p>状态{active?'显示中':'已隐藏'}</p>
    </>
  )
}

function Article({title,articleData}) {
  return (
    <div>
      <h2>{title}</h2>
      <Detail {...articleData}/>
    </div>
  )
}

export default function App(){
  const articleData = {
    title:'标题1',
    detailData:{
      content:'内容1',
      active:true
    }

  }
  return(
    <>  
      <Article
        {...articleData}
      />
    </>
  )
};

3.3.2react插槽功能

function List({children}){
  return(
    <ul>
      {children}
    </ul>
  )
}

export default function App(){
  return(
    <>  
      <List>
        <li>列表项1</li>
        <li>列表项1</li>
        <li>列表项1</li>
      </List>
      <List>
        <li>列表项2</li>
        <li>列表项2</li>
        <li>列表项2</li>
      </List>
    </>
  )
};

3.3.3react多处插槽

注意,对于可选值,例如下面代码中的footer,有可能有 有可能没有,这种情况要设置默认值

function List({children,title,footer=<div>默认底部</div>}){
  return(
    <>
      <h2>{title}</h2>
      <ul>
        {children}
      </ul>
      {footer}
    </>
  )
}


export default function App(){

  return(
    <>  
      <List
        title='标题1'
        footer={<p>这是底部内容1</p>}
      >
        <li>列表项1</li>
        <li>列表项1</li>
        <li>列表项1</li>
      </List>
      <List
        title='标题2'
        footer={<p>这是底部内容1</p>}
      >
        <li>列表项A</li>
        <li>列表项B</li>
        <li>列表项C</li>
      </List>
      <List
        title='标题3'
      >
        <li>列表项X</li>
        <li>列表项Y</li>
        <li>列表项Z</li>
      </List>
    </>
  )
};

3.3.4子传父

import {useState} from 'react'
function Detail({onActive}){
  const [status,setStatus] = useState(false)
  function handleClick(){
    setStatus(!status)
    onActive(status)
  }
  return(
    <div>
      <button onClick={handleClick}>按钮</button>
      <p style={{
        display:status?'block':'none'
      }}>Detail的内容</p>
    </div>
  )
}


export default function App(){
  function handleActive(status){
    console.log(status)
  }
  return(
    <>  
     <Detail 
      onActive={handleActive}
     />
    </>
  )
};

3.3.5同级组件传值


import {useContext, createContext, useState} from 'react'
export function Section({children}){
  const level = useContext(LevelContext)
  return(
    <section>
      <LevelContext.Provider value={level+1}>
        {children}
      </LevelContext.Provider>
    </section>
  )
}

export function Heading({children}){
  const level = useContext(LevelContext)
  switch(level){
    case 1:
      return <h1>{children}</h1>
    case 2:
      return <h2>{children}</h2>
    case 3:
      return <h3>{children}</h3>
    case 4:
      return <h4>{children}</h4>
    case 5:
      return <h5>{children}</h5>
    case 6:
      return <h6>{children}</h6>
    default:
      throw Error('未知的level:'+level)
  }
}

const LevelContext = createContext(1)

export default function App(){
  return(
    <div>
      <Section>
        <Heading>主标题</Heading>
        <Section>
          <Heading>副标题</Heading>
          <Heading>副标题</Heading>
          <Heading>副标题</Heading>
          <Section>
            <Heading>子标题</Heading>
            <Heading>子标题</Heading>
            <Heading>子标题</Heading>
            <Section>
              <Heading>子子标题</Heading>
              <Heading>子子标题</Heading>
              <Heading>子子标题</Heading>
            </Section>
          </Section>
        </Section>
      </Section>
    </div>
)
} 

LevelContext.Provider 提供一个上下文值,React 的 Context 机制 是 从外层向内层(自上而下)传递数据的。

因此上面的代码块运行过程如下:

  • 最外层的 Section 没有上级 Provider,所以使用默认值 1
  • 它内部的 Heading 就会渲染为 h1
  • 嵌套在里面的 Section 会把级别增加到 2,所以它的 Heading 会渲染为 h2
  • 再往内一层就是 h3,以此类推
  • <LevelContext.Provider value={level + 1}> 会动态更新 level,并且 内层的 Heading 组件会实时获取最新的 level 值,因为 useContext(LevelContext) 会自动订阅最近的 Provider 的变化

image-20250816210159737.png

4. React Hooks

4.1useReducer

用于管理比 useState 更复杂的状态逻辑,特别适合状态逻辑较复杂或包含多个子值的场景。

基本语法:

const [state, dispatch] = useReducer(reducer, initialState);
  • reducer:一个函数,形式为 (state, action) => newState
  • initialState:状态的初始值
import {useReducer} from 'react'

function countReducer (state,action){
  switch(action.type){
    case 'increment':
      return state + 1
    case 'decrement':
      return state - 1
    default:
      throw new Error()
  }
}

export default function App(){
  // 计算器
  const [count,dispatch] = useReducer(countReducer,0)
  const handleIncrement = () => dispatch({type:'increment'})
  const handleDecrement = () => dispatch({type:'decrement'})
  return(
    <div style={{padding:10}}>
      <button onClick={handleDecrement}>-</button>
      <span>{count}</span>
      <button onClick={handleIncrement}>+</button>
    </div>
  )
}
  1. 对于const [count,dispatch] = useReducer(countReducer,0):
  • 初始化状态:count 初始值为 0(第二个参数)。
  • 定义状态更新规则:countReducer 是一个函数,决定如何根据 action 更新 count
  • 返回当前状态和派发函数:
    • count:当前的状态值(这里是数字)。
    • dispatch:用于触发状态更新的函数(发送 actioncountReducer)。
  1. 对于const handleIncrement = () => dispatch({type:'increment'})
  • type属于一个只是一个普通的对象属性名,可以自由命名,例如写成handleDecrement → dispatch({ abc: 'decrement' }) 判断条件则写成action.abc = 'decrement'

4.2useRef

useRef 返回一个可变的 ref 对象({ current: undefined }),其 .current 属性可以存储任意值。ref 的变动不会触发组件重新渲染(与 state 不同)

import {useRef,useState} from 'react'

export default function App() {
  const [count, setCount] = useState(0)
  const prevCount = useRef()

  function handleClick() {
    prevCount.current = count // 在更新前,保存当前的 count 到 prevCount
    setCount(prevCount.current + 1)   // 更新 count
  }
  
  return(
    <div>
      <p>最新的count:{count}</p>
      <p>上一次的count:{prevCount.current}</p>
      <button onClick={handleClick}>增大count</button>
    </div>
  )
}

4.3useImperativeHandle & forwardRef

import {forwardRef, useRef, useImperativeHandle} from 'react'

const Child = forwardRef(function (props, ref) 
    // Child 组件被 forwardRef 包装,可以接收 ref
  useImperativeHandle(ref, () => ({
    // 子组件暴露给父组件的方法
    handleClick: () => {
      console.log('子组件方法')
    }
  }))
  return <div>子组件</div>
})

export default function App() {
  const childRef = useRef()
  function handleClick() {
    childRef.current.handleClick()
  }
  
  return(
    <div>
      <Child ref={childRef}/>
      <button onClick={handleClick}>点击</button>
    </div>
  )
}

4.4forwardRef

forwardRef 允许 子组件接收父组件传递的 ref,并将其绑定到子组件内部的 DOM 或方法上。

为什么需要 forwardRef

  • 默认情况下,函数组件不能直接接收 ref(因为函数组件没有实例)。
  • forwardRef 包装后,子组件可以接收 ref 并决定如何暴露内部内容。
  • props:父组件传递的属性。
  • ref:父组件传递的 ref 对象。

4.5useImperativeHandle

useImperativeHandle 允许 子组件自定义暴露给父组件的内容(如方法、属性),而不是直接暴露 DOM 节点。

为什么需要 useImperativeHandle

  • 默认情况下,ref 只能访问 DOM 节点(如 <div ref={ref}>)。

  • 使用 useImperativeHandle,可以暴露 子组件的特定方法或数据,而不是整个 DOM。

  • ref:父组件传递的 ref

  • 回调函数:返回一个对象,定义暴露的内容

4.6useEffect

用于在函数组件中执行副作用操作

执行时机:

  1. 没有依赖数组:每次渲染后都会执行

    useEffect(() => {
      console.log('每次渲染后执行');
    });
    
  2. 空依赖数组:仅在组件挂载时执行一次

    useEffect(() => {
      console.log('仅在组件挂载时执行');
    }, []);
    
  3. 有依赖项:依赖项变化时执行

    useEffect(() => {
      console.log('count变化时执行');
    }, [count]);
    

4.7useMemo

用于缓存计算结果,避免在每次渲染时都进行不必要的复杂计算

基本语法:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
  • 第一个参数:一个函数,返回需要缓存的值
  • 第二个参数:依赖项数组,只有当依赖项发生变化时才会重新计算

demo演示

import { useState } from "react";

function DoSomeMath({ value }){
  console.log('DoSomeMath执行了')
  let result = 0
  for(let i=0;i<1000000;i++){
      result += value * 2;
  }

  return(
      <div>
          <p>输入内容:{value}</p>
          <p>经过复杂计算的数据:{result}</p>
      </div>
  )
}

function App() {
  const [inputValue, setInputValue] = useState(5);
  const [count, setCount] = useState(0);

  return(
    <div>
      <p>count的值为:{count}</p>
      <button
        onClick={() => setCount(count + 1)}
      >点击更新</button>
      <br />
      <br />
      <input 
        type="number" 
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
      />
      <DoSomeMath value={inputValue} />
    </div>
  )
}

export default App;

在这个demo中,DoSomeMath相当于一个复杂计算,针对value进行计算。我们在DoSomeMath添加调试信息console.log('DoSomeMath执行了')会发现改变count值时,也会执行DoSomeMath复杂计算。

这时就需要使用到useMemo

function DoSomeMath({ value }){
  const result = useMemo(() => {
    console.log('DoSomeMath执行了')
    let result = 0
    for(let i=0;i<1000000;i++){
        result += value * 2;
    }
    return result
  },[value])

  return(
      <div>
          <p>输入内容:{value}</p>
          <p>经过复杂计算的数据:{result}</p>
      </div>
  )
}

要注意,需要添加[value]这个依赖项数组,表示当value发生变化后才会执行复杂计算

4.8useCallBack

用于缓存函数引用,在依赖项不变的情况下返回相同的函数实例,避免子组件不必要的重新渲染。

基本语法:

const memoizedCallback = useCallback(
  () => {
    // 函数逻辑
    doSomething(a, b);
  },
  [a, b], // 依赖项数组
);

demo演示

import { useCallback, useState, memo } from "react";

const Button=memo(function ({onClick}) {
  console.log('Button渲染了');
  return (
    <button onClick={onClick}>
      子组件
    </button>
  )
})

function App() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    console.log('点击了');
  }
  
  const handUpdate = () => {
    setCount(count + 1);
  }

  return(
    <div>
      <p>Count:{count}</p>
      <button onClick={handUpdate}>点击</button>
      <br />
      <Button onClick={handleClick} />
    </div>
  )
}

export default App

上面的demo例子,如果父组件App重新渲染会导致子组件也被迫重新渲染,即点击<button onClick={handUpdate}>点击</button>会同时执行handleClick

memo的作用是:memo 会记忆(memoize)组件,只有当它的 props 发生变化时才会重新渲染

想要避免父组件重新渲染时导致不必要的子组件重新渲染,还需要使用useCallBack函数

const handleClick = useCallback(() => {
    console.log('点击了');
},[])

demo的工作原理:

  1. 当点击"点击"按钮时:
    • count 状态更新
    • App 组件重新渲染
    • 如果没有 useCallback,每次渲染都会创建一个新的 handleClick 函数实例
    • 新的函数实例会导致 memo 认为 Button 的 props 发生了变化,从而重新渲染 Button
  2. 使用 useCallback 后:
    • handleClick 保持不变
    • memo 检测到 Button 的 props 没有变化
    • Button 不会重新渲染(控制台不会打印"Button渲染了")