React记录之context:useContext、use-context-selector

0 阅读3分钟

原生context、useContext详解

React 的 Context API 是一种组件间共享数据的机制,它允许你在组件树中传递数据而不必手动逐层传递 props,特别适合"全局"数据的共享(如主题、用户认证信息等)。

基本使用:

创建context:

import { createContext, useContext } from 'react';
​
export type ThemeType = 'light' | 'dark';
​
export interface ThemeContextType {
  theme: ThemeType;
  toggleTheme: () => void;
}
​
// 1. 创建 Context
export const ThemeContext = createContext<ThemeContextType>({
  theme: 'light',
  toggleTheme: () => {},
});
​
type ThemeProviderProps = {
  children: React.ReactNode;
} & ThemeContextType;
​
// 2. 创建 Provider 组件
export const ThemeProvider = ({
  children,
  theme,
  toggleTheme,
}: ThemeProviderProps) => {
  return (
    <ThemeContext.Provider value={{
      theme,
      toggleTheme,
    }}>
      {children}
    </ThemeContext.Provider>
  );
};
​
// 3. 自定义 Hook(可选,提升可读性)
export const useTheme = () => useContext(ThemeContext);

顶层组件 top.tsx

"use client";
​
import React, { useState } from 'react';
import { ThemeContext, ThemeContextType, ThemeType } from './context';
import Button from '../../components/button';
​
function App() {
  const [theme, setTheme] = useState<ThemeType>('light');
​
  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };
​
  const value: ThemeContextType = { theme, toggleTheme };
​
  return (
    <ThemeContext.Provider value={value}>
      <div
        style={{
          padding: '20px',
          background: theme === 'dark' ? '#000' : '#fff',
          color: theme === 'dark' ? '#fff' : '#000'
        }}
      >
        <h1>Current theme: {theme}</h1>
        <Button />
      </div>
    </ThemeContext.Provider>
  );
}
​
export default App;

Button组件

import React from 'react';
import { useTheme } from '../hook-api/use-context/context';
​
export default function Button() {
  const { theme, toggleTheme } = useTheme();
​
  return (
    <button onClick={toggleTheme}>Toggle Theme {theme}</button>
  );
}

使用场景

  • 全局主题(亮色/暗色模式)
  • 用户认证状态(登录用户信息)
  • 多语言国际化(i18n)
  • 全局配置或状态(如购物车、通知设置)

注意事项:

性能问题:当 Provider 的 value 发生变化时,所有使用该 Context 的子组件都会重新渲染(即使只用到部分字段)。为避免不必要的重渲染:

  • value 拆分为多个 Context;
  • 使用 useMemo 稳定 value 引用;
  • 将不依赖 Context 的子组件提取到 Provider 外部。

不要滥用:Context 不是万能的状态管理工具。对于复杂状态逻辑,建议结合 useReducer 或使用 Redux、Zustand 等状态库。

use-context-selector

use-context-selector 是一个 React 上下文(Context)优化库,它解决了 React 原生 useContext 在性能上的一个关键问题:当上下文值变化时,所有使用该上下文的组件都会重新渲染,即使它们只依赖上下文中的一小部分数据。

核心特性

  1. 选择性订阅:允许组件只订阅上下文中的特定部分数据
  2. 精确更新:只有当下文中的选定部分变化时才会触发组件更新
  3. 与原生Context API兼容:使用方式与React原生Context相似
  4. 轻量级:体积小,对应用包大小影响小

基本使用:

App.tsx

'use client'import React, { StrictMode } from 'react';
import { MyProvider } from './context';
import CounterA from './components/CounterA';
import CounterB from './components/CounterB';
​
function App() {
  return (
    <StrictMode>
      <MyProvider>
        <CounterA />
        <CounterB />
      </MyProvider>
    </StrictMode>
  );
}
​
export default App;

context.tsx

'use client'import { useState } from 'react';
import{ createContext } from 'use-context-selector';
​
const MyContext = createContext({} as any);
​
export function MyProvider({ children }: any) {
  const [countA, setCountA] = useState(0);
  const [countB, setCountB] = useState(0);
​
  const state: any = {
    countA,
    setCountA,
    countB,
    setCountB,
  };
​
  return (
    <MyContext.Provider value={state}>
      {children}
    </MyContext.Provider>
  );
}
​
export default MyContext;

CounterA.tsx

'use client'
​
​
import React from 'react';
import { useContextSelector, useContext } from 'use-context-selector';
import MyContext from '../context';
​
function CounterA() {
  const countA = useContextSelector(MyContext, (v) => v.countA);
  const setCountA = useContextSelector(MyContext, (v) => v.setCountA);
​
  const increment = () =>
    setCountA((s) => s -1);
​
  console.log('CounterA rendered');
​
  return (
    <div>
      <p>{new Date().getTime()}</p>
      <p>Counter A: {countA}</p>
      <button onClick={increment}>
        Increment A
      </button>
    </div>
  );
}
​
export default CounterA;

CounterB.tsx

'use client'import React from 'react';
import { useContextSelector, useContext } from 'use-context-selector';
import MyContext from '../context';
​
function CounterB() {
  const countB = useContextSelector(MyContext, (v) => v.countB);
  const setCountB = useContextSelector(MyContext, (v) => v.setCountB);
​
  const increment = () =>
    setCountB((s) => s -1);
​
  console.log('CounterB rendered');
​
  return (
    <div>
      <button onClick={increment}>
        Increment B
      </button>
      <p>{new Date().getTime()}</p>
      <p>Counter B: {countB}</p>
    </div>
  );
}
​
export default CounterB;