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 的变化,整个组件都会被重新刷新,一些函数是没有必要被重新刷新的,此时就应该缓存起来,提高性能,和减少资源浪费。
相关推荐
慧一居士4 分钟前
flex 布局完整功能介绍和示例演示
前端
DoraBigHead6 分钟前
小哆啦解题记——两数失踪事件
前端·算法·面试
一斤代码6 小时前
vue3 下载图片(标签内容可转图)
前端·javascript·vue
中微子6 小时前
React Router 源码深度剖析解决面试中的深层次问题
前端·react.js
光影少年6 小时前
从前端转go开发的学习路线
前端·学习·golang
中微子6 小时前
React Router 面试指南:从基础到实战
前端·react.js·前端框架
3Katrina6 小时前
深入理解 useLayoutEffect:解决 UI "闪烁"问题的利器
前端·javascript·面试
前端_学习之路7 小时前
React--Fiber 架构
前端·react.js·架构
coderlin_7 小时前
BI布局拖拽 (1) 深入react-gird-layout源码
android·javascript·react.js
甜瓜看代码7 小时前
1.
react.js·node.js·angular.js