React 核心 API 全景实战:从状态管理到性能优化,一网打尽

✨ 为什么写这篇文章?

很多前端朋友在用 React 的时候:

  • 只会用 useState 做局部状态,结果项目一大就乱套。
  • 不了解 useReducerContext,复杂页面全靠 props 一层层传。
  • 性能卡顿后,只知道用 React.memo,但为什么卡?
  • useMemouseCallback的区别 ?
  • 明明只是个 Modal,结果被卡在组件层级里动弹不得,不知道可以用 Portals

👉「在什么场景下选用哪个 API」+「如何写出最合理的 React 代码」。

🟢 1. useState:局部状态管理

🌳 场景:表单输入管理

比起枯燥的计数器,这里用表单输入做示例。

jsx 复制代码
import { useState } from 'react';

export default function LoginForm() {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = e => {
    e.preventDefault();
    console.log("登录中", username, password);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={username} onChange={e => setUsername(e.target.value)} placeholder="用户名"/>
      <input type="password" value={password} onChange={e => setPassword(e.target.value)} placeholder="密码"/>
      <button type="submit">登录</button>
    </form>
  );
}

🚀 优势

  • 简单、直接
  • 适用于小型、独立的状态

🟡 2. useEffect:副作用处理

🌍 场景:组件挂载时拉取远程数据

jsx 复制代码
import { useEffect, useState } from 'react';

export default function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch(`/api/user/${userId}`)
      .then(res => res.json())
      .then(data => {
         setUser(data);
      });

    return () => {
      // 组件销毁执行此回调
    };
  }, []);

  return user ? <h1>{user.name}</h1> : <p>加载中...</p>;
}

🚀 优势

  • 集中管理副作用(请求、订阅、定时器、事件监听)

🔵 3. useRef & useImperativeHandle:DOM、实例方法控制

场景 1:聚焦输入框

jsx 复制代码
import { useRef, useEffect } from 'react';

export default function AutoFocusInput() {
  const inputRef = useRef();

  useEffect(() => {
    inputRef.current.focus();
  }, []);

  return <input ref={inputRef} placeholder="自动聚焦" />;
}

场景 2:在父组件调用子组件的方法

jsx 复制代码
import { forwardRef, useRef, useImperativeHandle } from 'react';

const FancyInput = forwardRef((props, ref) => {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus()
  }));
  return <input ref={inputRef} />;
});

export default function App() {
  const fancyRef = useRef();
  return (
    <>
      <FancyInput ref={fancyRef} />
      <button onClick={() => fancyRef.current.focus()}>父组件聚焦子组件</button>
    </>
  );
}

🧭 4. Context & useContext:解决多层级传值

场景:用户登录信息在多层组件使用

jsx 复制代码
import React, { createContext, useContext } from 'react';

const UserContext = createContext();

/** 设置在 DevTools 中将显示为 User */
UserContext.displayName = 'User'

function Navbar() {
  return (
      <div>
          <UserInfo />
      </div>
  )
}

function UserInfo() {
  const user = useContext(UserContext);
  return <span>欢迎,{user.name}</span>;
}

export default function App() {
  return (
    <UserContext.Provider value={{ name: 'Zheng' }}>
      <Navbar />
    </UserContext.Provider>
  );
}

🚀 优势

  • 解决「祖孙组件传值太麻烦」的问题

🔄 5. useReducer:复杂状态管理

jsx 复制代码
import { useReducer } from 'react';

function reducer(state, action) {
  switch(action.type){
    case 'next':
      return { ...state, step: state.step + 1 };
    case 'prev':
      return { ...state, step: state.step - 1 };
    default:
      return state;
  }
}

export default function Wizard() {
  const [state, dispatch] = useReducer(reducer, { step: 1 });

  return (
    <>
      <h1>步骤 {state.step}</h1>
      <button onClick={() => dispatch({type: 'prev'})}>上一步</button>
      <button onClick={() => dispatch({type: 'next'})}>下一步</button>
    </>
  );
}

🆔 6. useId:避免 SSR / 并发下 ID 不一致

jsx 复制代码
import { useId } from 'react';

export default function FormItem() {
  const id = useId();
  return (
    <>
      <label htmlFor={id}>姓名</label>
      <input id={id} type="text" />
    </>
  );
}

jsx 复制代码
import { useState } from 'react';
import ReactDOM from 'react-dom';

function Modal({ onClose }) {
  return ReactDOM.createPortal(
    <div style={{ position: "fixed", top: 100, left: 100, background: "white" }}>
      <h1>这是 Modal</h1>
      <button onClick={onClose}>关闭</button>
    </div>,
    document.getElementById('root')
  );
}

export default function App() {
  const [show, setShow] = useState(false);
  return (
    <>
      <button onClick={() => setShow(true)}>打开 Modal</button>
      {show && <Modal onClose={() => setShow(false)} />}
    </>
  );
}

在上面代码中,我们将要渲染的视图作为createPortal方法的第一个参数,而第二个参数用于指定要渲染到那个DOM元素中。


尽管 portal 可以被放置在 DOM 树中的任何地方,但在任何其他方面,其行为和普通的 React 子节点行为一致。由于 portal 仍存在于 React 树, 且与 DOM 树中的位置无关,那么无论其子节点是否是 portal ,像 context 这样的功能特性都是不变的。

这包含事件冒泡。一个从 portal 内部触发的事件会一直冒泡至包含 React 树的祖先,即便这些元素并不是 DOM 树中的祖先。

🔍 8. 组件渲染性能优化

🐘 之前类组件时代:shouldComponentUpdate与PureComponent

jsx 复制代码
import { Component } from 'react'

export default class App extends Component {
  constructor() {
    super();
    this.state = {
      counter: 1
    }
  }
  render() {
    console.log("App 渲染了");
    return (
      <div>
        <h1>App 组件</h1>
        <div>{this.state.counter}</div>
        <button onClick={() => this.setState({
          counter : 1
        })}>+1</button>
      </div>
    )
  }
}

在上面的代码中,按钮在点击的时候仍然是设置 counter 的值为1,虽然 counter 的值没有变,整个组件仍然是重新渲染了的,显然,这一次渲染是没有必要的。

propsstate 发生变化时,shouldComponentUpdate 会在渲染执行之前被调用。返回值默认为 true。首次渲染或使用 forceUpdate 方法时不会调用该方法。

下面我们来使用 shouldComponentUpdate 优化上面的示例:

jsx 复制代码
import React from 'react'

/**
 * 对两个对象进行一个浅比较,看是否相等
 * obj1
 * obj2
 * 返回布尔值 true 代表两个对象相等, false 代表不相等
 */
function objectEqual(obj1, obj2) {
  for (let prop in obj1) {
    if (!Object.is(obj1[prop], obj2[prop])) {
      // 进入此 if,说明有属性值不相等
      // 只要有一个不相等,那么就应该判断两个对象不等
      return false
    }
  }
  return true
}

class PureChildCom1 extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      counter: 1,
    }
  }

  // 验证state未发生改变,是否会执行render
  onClickHandle = () => {
    this.setState({
      counter: Math.floor(Math.random() * 3 + 1),
    })
  }

  // shouldComponentUpdate 中返回 false 来跳过整个渲染过程。其包括该组件的 render 调用以及之后的操作。
  // 返回true 只要执行了setState都会重新渲染
  shouldComponentUpdate(nextProps, nextState) {
    if (
      objectEqual(this.props, nextProps) &&
      objectEqual(this.state, nextState)
    ) {
      return false
    }
    return true
  }

  render() {
    console.log('render')
    return (
      <div>
        <div>{this.state.counter}</div>
        <button onClick={this.onClickHandle}>点击</button>
      </div>
    )
  }
}

export default PureChildCom1
  • PureComponent 内部做浅比较:如果 props/state 相同则跳过渲染。

  • 不适用于复杂对象(如数组、对象地址未变)。


🥇 React.memo:函数组件记忆化

上面主要是优化类组件的渲染性能,那么如果是函数组件该怎么办呢?

React中为我们提供了memo高阶组件,只要 props 不变,就不重新渲染。

jsx 复制代码
const Child = React.memo(function Child({name}) {
  console.log("Child 渲染");
  return <div>{name}</div>;
});

🏷 useCallback:缓存函数引用,避免触发子组件更新

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

function Child({ onClick }) {
  console.log("Child 渲染")
  return <button onClick={onClick}>点我</button>;
}

const MemoChild = React.memo(Child);

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

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

  return (
    <>
      <div>{count}</div>
      <button onClick={() => setCount(count+1)}>+1</button>
      <MemoChild onClick={handleClick} />
    </>
  );
}

在上面的代码中,我们对Child组件进行了memo缓存,当修改App组件中的count值的时候,不会引起Child组件更新;使用了useCallback对函数进行了缓存,当点击Child组件中的button时也不会引起父组件的更新。


🔢 useMemo:缓存计算

某些时候,组件中某些值需要根据状态进行一个二次计算(类似于 Vue 中的计算属性),由于函数组件一旦重新渲染,就会重新执行整个函数,这就导致之前的二次计算也会重新执行一次。

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

function App() {
  const [count, setCount] = useState(1);
  const [val, setValue] = useState('');
  
  console.log("App render");
  // 使用useMemo缓存计算
  const getNum = useMemo(() => {
    console.log('调用了!!!!!');
    return count + 100;
  }, [count])

  return (
    <div>
      <h4>总和:{getNum()}</h4>
      <div>
        <button onClick={() => setCount(count + 1)}>+1</button>
        {/* 文本框的输入会导致整个组件重新渲染 */}
        <input value={val} onChange={event => setValue(event.target.value)} />
      </div>
    </div>
  );
}

export default App;

在上面的示例中,文本框的输入会导致整个 App 组件重新渲染,但是 count 的值是没有改变的,所以 getNum 这个函数也是没有必要重新执行的。我们使用了 useMemo 来缓存二次计算的值,并设置了依赖项 count ,只有在 count 发生改变时,才会重新执行二次计算。


面试题:useMemouseCallback 的区别及使用场景?

useMemouseCallback 接收的参数都是一样,第一个参数为回调,第二个参数为要依赖的数据。

共同作用: 仅仅依赖数据发生变化,才会去更新缓存。

两者区别:

  1. useMemo 计算结果是 return 回来的值, 主要用于缓存计算结果的值。应用场景如:需要进行二次计算的状态
  2. useCallback 计算结果是函数, 主要用于缓存函数,应用场景如: 需要缓存的函数,因为函数式组件每次任何一个 state 的变化,整个组件都会被重新刷新,一些函数是没有必要被重新刷新的,此时就应该缓存起来,提高性能,和减少资源浪费。
相关推荐
辻戋1 小时前
从零实现React Scheduler调度器
前端·react.js·前端框架
徐同保1 小时前
使用yarn@4.6.0装包,项目是react+vite搭建的,项目无法启动,报错:
前端·react.js·前端框架
Qrun2 小时前
Windows11安装nvm管理node多版本
前端·vscode·react.js·ajax·npm·html5
中国lanwp2 小时前
全局 npm config 与多环境配置
前端·npm·node.js
JELEE.3 小时前
Django登录注册完整代码(图片、邮箱验证、加密)
前端·javascript·后端·python·django·bootstrap·jquery
TeleostNaCl5 小时前
解决 Chrome 无法访问网页但无痕模式下可以访问该网页 的问题
前端·网络·chrome·windows·经验分享
前端大卫6 小时前
为什么 React 中的 key 不能用索引?
前端
你的人类朋友6 小时前
【Node】手动归还主线程控制权:解决 Node.js 阻塞的一个思路
前端·后端·node.js
小李小李不讲道理8 小时前
「Ant Design 组件库探索」五:Tabs组件
前端·react.js·ant design
毕设十刻8 小时前
基于Vue的学分预警系统98k51(程序 + 源码 + 数据库 + 调试部署 + 开发环境配置),配套论文文档字数达万字以上,文末可获取,系统界面展示置于文末
前端·数据库·vue.js