React Hooks(数据驱动、副作用、状态传递、状态派生)

React hook

参考资料

  1. BV1mcpPeMETt
  2. https://message163.github.io/react-docs/react/basic/development.html

前置知识

所有的 hook 都要在组件的最顶层使用

1. 数据驱动

useState

  • 例子(在这里点击了按钮之后,变量会改变,但是视图不会改变

    也就是说,数据与视图有挂钩,就必须用 useState

使用方法

基本数据类型
javascript 复制代码
import './App.css'
import { useState } from 'react'

function App() {
  let [str, setStr] = useState('test1')
  const handleClick = () => {
    setStr('test2')
  }
  return (
    <>
      <div>{str}</div>
      <button onClick={handleClick}>
        点击
      </button>
    </>
  )
}

export default App
复杂数据类型
数组

在 React 中你需要将数组视为只读的,不可以直接修改原数组,例如:不可以调用 arr.push() arr.pop() 等方法。

解决方法:解构一下

javascript 复制代码
import { useState } from 'react'

function App() {
  let [arr, setArr] = useState([1,2,3])
  const handleClick = () => {
    // 解构
    setArr([...arr, 4])
  }
  return (
    <>
      <div>{arr}</div>
      <button onClick={handleClick}>
        点击
      </button>
    </>
  )
}

export default App
  • 如果是想在指定位置添加一个元素呢?注意不能用 splice,因为 splice 是修改原数组。
javascript 复制代码
import { useState } from 'react'

function App() {
  let [arr, setArr] = useState(['1','2','3'])
  const handleClick = () => {
    const start = 0;
    const end = 2;
    setArr([
      ...arr.slice(start, end),
      '-2.5-',
      ...arr.slice(end)
    ])
  }
  return (
    <>
      <div>{arr}</div>
      <button onClick={handleClick}>
        点击
      </button>
    </>
  )
}
  • 排序:先复制一个新数组,然后排序,然后用 set
javascript 复制代码
import { useState } from 'react'

function App() {
  let [arr, setArr] = useState([3,2,5,4,1])
  const handleClick = () => {
    // 复制一份新的数组
    let nextList = [...arr];
    nextList.sort((a, b) => b - a);
    setArr(nextList)
  }
  return (
    <>
      <div>{arr}</div>
      <button onClick={handleClick}>
        点击
      </button>
    </>
  )
}

export default App
对象

useState 里可以写函数,但是一定要返回值

函数只会初始化执行一次

  • 如何修改对象?
    下面这种写法,会让date age消失

    方法一:解构,下面的值 会覆盖上面的同名

    方法二:Object.assign

更新机制

useState set函数是异步更新的

  • 作用:优化渲染性能

    因为调用 set 函数回触发组件的重新渲染

    这里只会渲染最终的 3,在队列里面拿到最新的值

  • 问题:

  • 这是因为队列里面会判断是否有重复性操作

  • 解决方案:set 里面支持接收回调函数,回调函数里面会接收上一次的值

javascript 复制代码
import { useState } from 'react'

function App() {
  let [index, setIndex] = useState(0)
  const handleClick = () => {
    setIndex((prevIndex) => prevIndex + 1)
    setIndex((prevIndex) => prevIndex + 1)
    setIndex((prevIndex) => prevIndex + 1)
    // setIndex(index + 1)
    // setIndex(index + 1)
    console.log('index:', index)
  }
  return (
    <>
      <h1>index: {index}</h1>
      <button onClick={handleClick}>点击</button>
    </>
  )
}

export default App

useReducer

是集中式管理状态,

  • useReducer 接受的三个参数
  1. 第 2 个参数 initialArg 是 state 的默认值

  2. 第 3 个参数 init 是可选的,是一个初始化函数 ,用来修饰默认值。

    a. init 的参数是 initialState(第二个参数)

    b. init 必须返回"最终的初始 state"

    c. 只执行一次

    d. 如果编写了init函数,则默认值使用init函数的返回值,否则使用initialArg。

  3. 第 1 个参数 reducer 是一个处理函数,用于更新状态

    a. reducer 必须返回一个新的 state (不能直接修改原来的 state)

    b. 两个参数,第一个参数是 state,第二个参数是 action

    c. reducer 函数通过 dispatch 触发。

  • useReducer 返回值
    当前的 state。初次渲染时,它是 init(initialArg) 或 initialArg (如果没有 init 函数)。
    dispatch 函数。用于更新 state 并触发组件的重新渲染。

例子: 计数器

javascript 复制代码
import './App.css'
import { useReducer } from 'react'

function App() {
  const initialArg = {count: -1};
  const init = (state) => {
    return {
      count: Math.abs(state.count)
    }
  }

  const reducer = (state, action: {type: 'add' | 'sub'}) => {
    switch(action.type) {
      case 'add':
        return {
          count: state.count + 1
        }
      case 'sub':
        return {
          count: state.count - 1
        }
      default:
        return state
    }

  }
  const [state, dispath] = useReducer(reducer, initialArg, init);
  return (
    <>
      <h1>index: {state.count}</h1>
      <button onClick={() => dispath({type: 'sub'})}>-</button>
      <button onClick={() => dispath({type: 'add'})}>+</button>
    </>
  )
}

export default App

购物车

javascript 复制代码
import './App.css'
import { useReducer } from 'react'

function App() {
  const initData = [
    { name: '苹果', price: 100, count: 1, id: 1, isEdit: false },
    { name: '菠萝', price: 200, count: 1, id: 2, isEdit: false },
    { name: '梨子', price: 300, count: 1, id: 3, isEdit: false }
  ]

  type Data = typeof initData;
  const reducer = (state: Data, action: {type: 'add' | 'sub' | 'delete' | 'edit' |'update_name' | 'blur', id: number, newName?: string}) => {
    const item = state.find(item => item.id === action.id)!
    console.log(item);
    switch(action.type) {
      case 'add':
        item.count += 1
        return [...state]
      case 'sub':
        item.count -= 1
        return [...state]
      case 'delete':
        return state.filter(item => item.id !== action.id)
      case 'edit':
        item.isEdit = !item.isEdit
        return [...state]
      case 'update_name':
        item.name = action.newName!
        return [...state]
      case 'blur':
        item.isEdit = false
        return [...state]
      default:
        return state
    }
  }

  const [data, dispatch] = useReducer(reducer, initData)
  return (
    <>
      <h1>购物车</h1>
      <table border={1} width={600}>
        <thead>
          <tr>
            <th>商品</th>
            <th>单价</th>
            <th>数量</th>
            <th>总价</th>
            <th>操作</th>
          </tr>
        </thead>
        <tbody>
          {
            data.map((item) => {
              return <tr key = {item.id}>
                <td>{
                    item.isEdit ?
                    <input type='text' value={item.name}
                      onChange={(e) => dispatch({type: 'update_name', id: item.id, newName: e.target.value})}
                      onBlur={() => dispatch({type: 'blur', id: item.id})}>
                    </input>
                     :
                    item.name
                  }
                </td>
                <td>{item.price}</td>
                <td>
                  <button onClick={() => dispatch({type: 'add', id: item.id})}>+</button>
                  {item.count}
                  <button onClick={() => dispatch({type: 'sub', id: item.id})}>-</button>
                </td>
                <td>{item.price * item.count}</td>
                <td>
                  <button onClick={() => dispatch({type: 'edit', id: item.id})}>修改</button>
                  <button onClick={() => dispatch({type: 'delete', id: item.id})}>删除</button>
                </td>
              </tr>
            })
          }
        </tbody>
        <tfoot>
          <tr>
            <td colSpan={4}></td>
            <td>总价: {data.reduce((a, b) => a + b.price * b.count, 0)}</td>
          </tr>
        </tfoot>
      </table>
    </>
  )
}

export default App

2. 副作用

useEffect 是 React 中用于处理副作用的钩子。

useEffect

副作用函数 & 纯函数

  • 纯函数
  1. 输入决定输出:相同的输入永远会得到相同的输出。这意味着函数的行为是可预测的。
  2. 无副作用:纯函数不会修改外部状态,也不会依赖外部可变状态。因此,纯函数内部的操作不会影响外部的变量、文件、数据库等。
  • 副作用函数
  1. 副作用函数 指的是那些在执行时会改变外部状态或依赖外部可变状态的函数。
  2. 可预测性降低但是副作用不一定是坏事有时候副作用带来的效果才是我们所期待的
  3. 高耦合度函数非常依赖外部的变量状态紧密

使用

useEffect(effectCallback, dependencyArray?)

  • 参数
    ① 第一个参数:effect。effect 回调可以 return 一个函数。
    ② 第二个参数:deps(依赖数组,可选)
  • 返回值
    ① useEffect 返回 undefined
    let a = useEffect(() => {})
    console.log('a', a) //undefined
    ② useEffect 的回调函数如果返回一个函数,这个返回的函数会被当作「清理函数(cleanup)」

⭐ 执行时机

  1. 情况1:组件 初始化 + 组件 更新,像 DidMount DidUpdate 的时机
  2. 情况2:依赖项发生变化才会执行,空数组的情况只走一次(初始化、详情页数据)
javascript 复制代码
import { useEffect, useState } from 'react'

function App() {
  const [count, setCount] = useState(0)
  useEffect(() => {
    console.log('count变化了', count)
  }, [count])
  return (
    <>
      <div>
        <button onClick={() => setCount(count+1)}>111</button>
      </div>
    </>
  )
}

export default App
  1. ①组件卸载的时候执行 清理函数。②组件更新之前也会执行上一次的清理函数
javascript 复制代码
useEffect(() => {
  // 副作用逻辑
  return () => {
    // 清理副作用
  }
}, [])
javascript 复制代码
import { useEffect, useState } from 'react'

const Child = (props: any) => {
  useEffect(() => {
    console.log('child组件被创建了')
    return () => {
      console.log('child组件被销毁了')
    }
  }, [props.name])
  return <div>child</div>
}

function App() {
  const [isShow, setIsShow] = useState(true)
  return (
    <>
      <div>
        <button onClick={() => setIsShow(!isShow)}>111</button>
        {
          isShow && <Child name="child组件" />
        }
      </div>
    </>
  )
}

export default App

对于这一句话【②组件更新之前也会执行上一次的清理函数】的使用场景如下

props.name 变化时,React 会:

先执行「上一次 useEffect 的 cleanup」→ 再执行「新的 useEffect」

案例

下面是一个真实的用户信息获取案例,通过id获取用户信息,并且当id发生改变时,会获取新的用户信息。

javascript 复制代码
import './App.css'
import { useEffect, useState } from 'react'

interface UserData {
  name: string;
  email: string;
  username: string;
  phone: string;
  website: string;
}

function App() {
  const [userData, setUserData] = useState<UserData | null>(null);
  const [loading, setLoading] = useState<boolean>(true);
  const [userId, setUserId] = useState(1);
  useEffect(() => {
    setLoading(true);
    fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
      .then(response => response.json())
      .then(data => {
        setUserData(data);
        setLoading(false);
      })
  }, [userId])
  return (
    <>
    <div>
      <input type="text" value={userId} onChange={(e) => setUserId(e.target.value)}/>
      <div>
        {loading ? (
          <p>Loading...</p>
        ) : (
          <div>
            <p>Name: {userData?.name}</p>
            <p>Email: {userData?.email}</p>
            <p>Username: {userData?.username}</p>
            <p>Phone: {userData?.phone}</p>
            <p>Website: {userData?.website}</p>
          </div>
        )}
      </div>
    </div>
    </>
  )
}

export default App

useLayoutEffect

区别

useEffect 是在浏览器完成绘制后执行,不会阻塞渲染,适合绝大多数副作用,比如请求和订阅。

useLayoutEffect 在 DOM 更新完成后、浏览器绘制(paint)之前同步执行。其实是 布局之后,绘制之前。属于同步,会阻塞渲染,适合需要读取 DOM 或同步更新布局的场景,比如避免闪动、测量布局。

使用场景

场景:看商品详情页,点击进去,然后返回到详情页,如果有滚动条记录,会提升用户的使用体验

实现:滚动的时候把滚动条位置记录在 地址栏中,当刷新地址栏,就更新

javascript 复制代码
function App() {
  const scrollHander = (e) => {
    const scrollTop = e.currentTarget.scrollTop
    console.log('scrollTop', scrollTop);
    window.history.replaceState(null, '', `?top=${scrollTop}`)
  }

  useLayoutEffect(() => {
    const params = new URLSearchParams(window.location.search)
    const top = params.get('top')
    if (top) {
      const container = document.getElementById('container')
      container.scrollTop = parseInt(top)
    }
  })

  return (
    <div onScroll={scrollHander} id="container" style={{height: '400px', overflow: 'auto'}}>
      {
        Array.from({length: 500}).map((item, index) => {
          return <div key={index}>item - {index + 1}</div>
        })
      }
    </div>
  );
}

export default App;

3. 状态传递

useRef

作用

1.操作DOM元素(通过 ref 操作 dom 元素)

  1. 数据存储

作用1:操作DOM元素

语法:

javascript 复制代码
import { useRef } from 'react';
const refValue = useRef(initialValue)
refValue.current // 访问ref的值 类似于vue的ref,Vue的ref是.value,其次就是vue的ref是响应式的,而react的ref不是响应式的

案例:

javascript 复制代码
import React, { useRef } from 'react';

function App() {
  const divRef = useRef<HTMLDivElement>(null);

  const handleDOM = () => {
  	console.log(divRef.current)
    console.dir(divRef.current)
    divRef.current!.style.backgroundColor = 'red';
    // 这个感叹号是非空断言操作符,告诉TypeScript我们确定divRef.current不会是null
  }
  return (
    <div>
      <div>app </div>
      <div ref={divRef}>1111111111111111111111111</div>
      <button onClick={handleDOM}>操作按钮</button>
    </div>
  );
}

export default App;

作用2:数据存储

组件在重新渲染的时候,useRef 的值不会被重新初始化。

javascript 复制代码
import React, { useRef, useState } from 'react';

function App() {
  console.log('render');

  let num = useRef(0); // 组件在重新渲染的时候,useRef的值不会被重新初始化。
  // let num = 0; 这样写是不对的,因为每次组件重新渲染时,num都会被重新初始化为0,而useRef创建的ref对象在组件的整个生命周期内是持久的。
  const [count, setCount] = useState(0);
  const handleClick = () => {
    setCount(count + 1);
    num.current = count;
  }
  return (
    <div>
      <h1>数据存储</h1>
      <div>{count}: {num.current}</div>
      <button onClick={handleClick}>增加</button>
    </div>
  );
}

export default App;
  • 注意事项
  1. 组件在重新渲染的时候,useRef的值不会被重新初始化。
  2. 改变 ref.current 属性时,React 不会重新渲染组件。React 不知道它何时会发生改变,因为 ref 是一个普通的 JavaScript 对象。
  3. useRef的值不能作为useEffect等其他hooks的依赖项,因为它并不是一个响应式状态。
  4. useRef不能直接获取子组件的实例,需要使用forwardRef。

useContext

useContext 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。设计的目的就是解决组件树间数据传递的问题。

  • 用法
    React.createContext 创建一个上下文
javascript 复制代码
const MyThemeContext = React.createContext({theme: 'light'}); // 创建一个上下文
function App () {
   return (
      <MyThemeContext.Provider value={{theme: 'light'}}>
         <MyComponent />
      </MyThemeContext.Provider>
   )
}
function MyComponent() {
    const themeContext = useContext(MyThemeContext); // 使用上下文
    return (<div>{themeContext.theme}</div>);
}

案例

(1)使用的时候,先创建一个上下文传递值

javascript 复制代码
const ThemeContext = React.createContext({} as ThemeContextType);

这里会需要 TS 的类型声明,

(2)共享给谁就包裹谁

javascript 复制代码
<ThemeContext value={{ theme, setTheme }}>
    <Parent /> 这里 Parent 的子组件也共享到了
 </ThemeContext>

(3)使用的时候,用 useContext,指定上下文的名字

javascript 复制代码
const theme = useContext(ThemeContext);

(4)react 18 19 的写法区别是有无 Provider

  • 全部代码:
javascript 复制代码
import React, { useState, useContext } from 'react';

interface ThemeContextType {
   theme: string;
   setTheme: (theme: string) => void;
}

// 1 创建全局上下文(想要 App 共享给别人的数据
const ThemeContext = React.createContext({} as ThemeContextType);
// 返回的一个组件 ThemeContext

const Child = () => {
   const theme = useContext(ThemeContext);
   console.log(theme, 'Child');
   const styles = {
      width: '100px',
      height: '100px',
      backgroundColor: theme.theme === 'dark' ? '#000' : '#fff',
      color: theme.theme === 'dark' ? '#fff' : '#000',
      border: '1px solid #f5f5f5',
   }
   return <div>
      <div style={styles}>
         child
      </div>
   </div>
}

const Parent = () => {
   const theme = useContext(ThemeContext);
   console.log(theme, 'Parent');
   const styles = {
      width: '100px',
      height: '100px',
      backgroundColor: theme.theme === 'dark' ? '#000' : '#fff',
      color: theme.theme === 'dark' ? '#fff' : '#000',
      border: '1px solid #f5f5f5',
   }
   return <div>
      <div style={styles}>
         parent
      </div>
      <Child />
   </div>
}

function App() {
   const [theme, setTheme] = useState('light');
   return (
      <div>
         <button onClick={() => setTheme(theme === 'light' ? 'dark': 'light')}>切换</button>
         <ThemeContext value={{ theme, setTheme }}>
            <Parent />
         </ThemeContext>
      </div>
   );
}

export default App;

注意事项

(1)多层时候,同一个context 内层的会覆盖外层,如果使用的值是相同的,那么会覆盖。

javascript 复制代码
function App() {
   const [theme, setTheme] = useState('light');
   return (
      <div>
         <button onClick={() => setTheme(theme === 'light' ? 'dark': 'light')}>切换</button>
         <ThemeContext value={{ theme, setTheme }}>
            <ThemeContext value={{ theme:'aaa', setTheme }}>
               <Parent />
            </ThemeContext>
         </ThemeContext>
      </div>
   );
}

(2)传递的时候,他的key 只能叫 value

4. 状态派生

useMemo

React.memo

React.memo 是一个 API

  • 作用
    用于优化性能
    仅当 props 发生变化时才会重新渲染, 避免重新渲染。
  • 使用
javascript 复制代码
const MyComponent = React.memo(({ prop1, prop2 }) => {
  // 组件逻辑
});
  • 首先明确 React 组件的渲染条件:
    组件的 props 发生变化
    组件的 state 发生变化
    useContext 发生变化
案例

像这一个案例,每一次input输入变化,都会导致,子组件 UserCard 的重新渲染,但其实是不需要的,因为

javascript 复制代码
import React, { useState, useContext } from 'react';

interface User {
   name: string;
   age: number;
   phone: string;
}
const UserCard = (props: {user: User}) => {
   console.log('render UserCard');
   const {user} = props;

   const styles = {
      backgroundColor: 'lightblue',
      padding: '20px',
      borderRadius: '10px',
      margin: '10px'
   }

   return <div style={styles}>
      <p>姓名:{user.name}</p>
      <p>年龄:{user.age}</p>
      <p>电话:{user.phone}</p>
   </div>
}
function App() {
   const [input, setInput] = useState('');
   const [user, setUser] = useState({
      name: '张三',
      age: 18,
      phone: '1111111'
   });

   return (
      <>
         <input type="text" value={input} onChange={(e) => setInput(e.target.value)}/>
         <UserCard user={user}/>
      </>
   );
}

export default App;

修改后

useMemo

缓存上一次的值,只有当它里面的依赖项发生改变的时候,才会重新计算(像 vue computed)

useCallback

  • React 组件的渲染条件:
    组件的 props 发生变化
    组件的 state 发生变化
    useContext 发生变化

组件重新渲染就是包括它里面的函数进行销毁再重新创建的过程,这样性能并不好,useCallback 它是用于缓存组件内的函数,避免函数的重复创建。

但是不可以滥用,因为它是存于内存中的,会一直去占用内存。

  • 参数
    ① 回调函数
    ② 依赖数组

案例 1

javascript 复制代码
import React, { useState, useCallback } from 'react';

const map = new Map();
let count = 1;

function App() {
   const [input, setInput] = useState('');
   const changeValue = useCallback((e) => {
      setInput(e.target.value);
   }, [])
   // const changeValue = (e) => {
   //    setInput(e.target.value);
   // }

   if(!map.has(changeValue)) {
      // 能测试出来函数被销毁了
      map.set(changeValue, count++);
   }
   console.log(map.get(changeValue));

   return (
      <>
         <input type="text" value={input} onChange={changeValue}/>
      </>
   )
}

export default App;

案例 2

父组件有一个 input,然后传递 user 值给子组件,当用户在输入框输入的时候,虽然 input 值会改变,父组件也会重新渲染,但是子组件我们用 React.memo 包裹,于是子组件不会渲染。

现在新增一个 callback,父组件传递 callback 给 子组件,然后 我们在输入框输入,子组件竟然重新渲染了

这是因为父组件重新渲染之后,里面的callback函数地址也改变了,所以子组件每次也会重新渲染。

那么解决方法就是用 useCallback

javascript 复制代码
import React, { useState, useCallback } from 'react';

interface Props {
   user: {
      name: string,
      age: number
   }
   callback: () => void
}
const Child = React.memo((props: Props) => {
   console.log('render Child');

   return <>
      <div>姓名:{props.user.name}</div>
      <div>年龄:{props.user.age}</div>
   </>
})

function App() {
   const [input, setInput] = useState('');
   const [user, setUser] = useState({
      name: '张三',
      age: 18
   });

   // const callback = () => {
   //    console.log('callback');
   // }
   const callback = useCallback(() => {
      console.log('callback');
   },[])
   
   return (
      <>
         input在输入的时候child不会重新渲染,因为这里使用了 React.memo
         <input type="text" value={input} onChange={e => setInput(e.target.value)}/>
         <Child user={user} callback={callback}/>
      </>
   )
}

export default App;

useMemo & useCallback 区别

  • 共同点
    入参一样
  • 不同点
    ① 返回值不同,useCallback 返回当前缓存的这个函数;useMemo 返回函数执行之后的一个结果
    ② 使用场景,useCallback 适合缓存一些函数;useMemo 适合缓存大量计算的值

5. 工具 hooks

相关推荐
呆头鸭L9 小时前
用vue3+ts+elementPlus+vite搭建electron桌面端应用
前端·vue.js·electron
IT_陈寒9 小时前
2025年React生态最新趋势:我从Redux迁移到Zustand后性能提升40%的心得
前端·人工智能·后端
前端小臻9 小时前
react没有双向数据绑定是怎么实现数据实时变更的
前端·javascript·react.js
困惑阿三9 小时前
CSS 动效交互实验室
前端·css
哟哟耶耶9 小时前
随笔小计-前端经常接触的http响应头(跨域CORS,性能-缓存-安全,token)
前端·网络协议·http
Allen_LVyingbo9 小时前
病历生成与质控编码的工程化范式研究:从模型驱动到系统治理的范式转变
前端·javascript·算法·前端框架·知识图谱·健康医疗·easyui
rgeshfgreh10 小时前
Python函数全解析:定义、参数与作用域
前端·数据库·python
Serendipity-Solitude10 小时前
使用HTML创建井字棋
前端·html