MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

React Context 在函数组件中的应用

2022-08-153.8k 阅读

React Context 基础概念

在 React 应用中,数据通常通过 props 由父组件传递到子组件。但是,对于一些共享数据,如当前用户信息、主题设置等,如果应用层级较深,通过 props 逐层传递会变得繁琐且难以维护,这时候 React Context 就派上用场了。

React Context 提供了一种在组件树中共享数据的方式,而不必通过 props 一层一层地传递。它允许我们创建一个“上下文”对象,任何组件都可以从中读取数据或订阅数据变化,而无需关心其在组件树中的位置。

创建 Context

要使用 React Context,首先需要通过 createContext 方法创建一个 Context 对象。createContext 接受一个默认值作为参数,这个默认值会在消费组件(consuming component)所处的组件树中没有匹配的 Provider 时使用。

以下是创建 Context 的代码示例:

import React from 'react';

// 创建一个 Context 对象
const MyContext = React.createContext('default value');

export default MyContext;

在上述代码中,React.createContext('default value') 创建了一个名为 MyContext 的 Context 对象,并且设置了默认值为 default value

Context.Provider

Context.Provider 是一个 React 组件,它接收一个 value 属性,并将这个值提供给其后代组件。任何位于 Provider 组件树内的消费组件都可以访问到这个 value

import React from 'react';
import MyContext from './MyContext';

function App() {
  const sharedValue = 'actual value';
  return (
    <MyContext.Provider value={sharedValue}>
      {/* 子组件树 */}
    </MyContext.Provider>
  );
}

export default App;

App 组件中,通过 MyContext.ProvidersharedValue 作为 value 传递下去。子组件树中的消费组件就可以获取到这个 sharedValue

在函数组件中消费 Context

使用 useContext Hook

在函数组件中,我们可以使用 useContext Hook 来消费 Context。useContext 接收一个 Context 对象作为参数,并返回该 Context 的当前值。

假设我们有一个 ChildComponent 组件,它需要使用 MyContext 中的值:

import React, { useContext } from'react';
import MyContext from './MyContext';

function ChildComponent() {
  const contextValue = useContext(MyContext);
  return (
    <div>
      <p>The value from context is: {contextValue}</p>
    </div>
  );
}

export default ChildComponent;

ChildComponent 中,通过 useContext(MyContext) 获取到了 MyContext 的当前值,并将其渲染到页面上。如果 MyContextProvider 中的 value 发生变化,ChildComponent 会重新渲染,显示新的值。

多个 Context 的使用

在实际应用中,可能会有多个 Context。例如,我们可能有一个用于主题设置的 ThemeContext 和一个用于用户认证信息的 AuthContext

首先创建两个 Context:

import React from'react';

const ThemeContext = React.createContext('light');
const AuthContext = React.createContext({
  isAuthenticated: false,
  user: null
});

export { ThemeContext, AuthContext };

然后在父组件中提供这些 Context 的值:

import React from'react';
import { ThemeContext, AuthContext } from './contexts';
import ChildComponent from './ChildComponent';

function App() {
  const theme = 'dark';
  const auth = {
    isAuthenticated: true,
    user: { name: 'John' }
  };
  return (
    <ThemeContext.Provider value={theme}>
      <AuthContext.Provider value={auth}>
        <ChildComponent />
      </AuthContext.Provider>
    </ThemeContext.Provider>
  );
}

export default App;

ChildComponent 中消费这两个 Context:

import React, { useContext } from'react';
import { ThemeContext, AuthContext } from './contexts';

function ChildComponent() {
  const theme = useContext(ThemeContext);
  const auth = useContext(AuthContext);
  return (
    <div>
      <p>The current theme is: {theme}</p>
      <p>User is authenticated: {auth.isAuthenticated? 'Yes' : 'No'}</p>
    </div>
  );
}

export default ChildComponent;

这样,ChildComponent 就可以同时获取到 ThemeContextAuthContext 的值。

Context 与性能优化

虽然 Context 非常方便,但在使用时也需要注意性能问题。因为每当 Providervalue 属性发生变化时,所有消费该 Context 的组件都会重新渲染。

减少不必要的重新渲染

如果 Providervalue 是一个对象或数组,每次重新渲染 Provider 时,即使对象或数组的内容没有改变,也会导致消费组件重新渲染。为了避免这种情况,可以使用 useMemo Hook 来 memoize value

例如:

import React, { useMemo } from'react';
import MyContext from './MyContext';

function App() {
  const data = { key: 'value' };
  const memoizedData = useMemo(() => data, []);
  return (
    <MyContext.Provider value={memoizedData}>
      {/* 子组件树 */}
    </MyContext.Provider>
  );
}

export default App;

在上述代码中,useMemo(() => data, []) 确保只有在依赖数组(这里为空数组,表示永远不会因为依赖变化而重新计算)发生变化时,才会重新计算 memoizedData。这样可以避免因为 data 对象引用变化而导致的不必要的重新渲染。

细粒度的 Context

另一种优化方式是创建细粒度的 Context。不要将所有共享数据都放在一个 Context 中,而是根据数据的变化频率和相关性创建多个 Context。这样,某个 Context 的变化不会影响到不依赖它的组件。

例如,如果我们有用户信息和主题设置,用户信息可能经常变化,而主题设置可能很少变化。我们可以将它们分别放在不同的 Context 中:

import React from'react';

const UserContext = React.createContext(null);
const ThemeContext = React.createContext('light');

function App() {
  const user = { name: 'John' };
  const theme = 'dark';
  return (
    <UserContext.Provider value={user}>
      <ThemeContext.Provider value={theme}>
        {/* 子组件树 */}
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
}

export default App;

这样,当用户信息变化时,只有依赖 UserContext 的组件会重新渲染,而依赖 ThemeContext 的组件不会受到影响。

Context 的更新机制

Providervalue 发生变化时,消费该 Context 的组件会重新渲染。这是因为 React 内部会检测到 Providervalue 引用发生了变化,从而触发更新。

手动触发 Context 更新

有时候,我们需要在组件内部手动触发 Context 的更新。一种常见的做法是将更新函数作为 value 的一部分传递给 Provider

例如,我们有一个 CounterContext,用于在组件间共享一个计数器的值,并且提供一个增加计数器的方法:

import React, { useState } from'react';

const CounterContext = React.createContext({
  count: 0,
  increment: () => {}
});

function App() {
  const [count, setCount] = useState(0);
  const increment = () => setCount(count + 1);
  return (
    <CounterContext.Provider value={{ count, increment }}>
      {/* 子组件树 */}
    </CounterContext.Provider>
  );
}

export default App;

然后在消费组件中:

import React, { useContext } from'react';
import CounterContext from './CounterContext';

function CounterComponent() {
  const { count, increment } = useContext(CounterContext);
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

export default CounterComponent;

CounterComponent 中,通过 useContext(CounterContext) 获取到 countincrement 函数。点击按钮时,调用 increment 函数,会更新 CounterContextvalue,从而导致 CounterComponent 重新渲染。

Context 与 TypeScript

在使用 TypeScript 开发 React 应用时,结合 Context 可以增强代码的类型安全性。

定义 Context 的类型

首先,我们需要定义 Context 的值的类型。例如,对于一个用户信息的 Context:

import React from'react';

interface User {
  name: string;
  age: number;
}

interface UserContextType {
  user: User | null;
  setUser: (user: User | null) => void;
}

const UserContext = React.createContext<UserContextType>({
  user: null,
  setUser: () => {}
});

export default UserContext;

在上述代码中,我们定义了 User 接口表示用户信息,UserContextType 接口表示 UserContext 的值的类型,包括用户信息和设置用户信息的函数。

在函数组件中使用带类型的 Context

在消费组件中:

import React, { useContext } from'react';
import UserContext from './UserContext';

function UserComponent() {
  const { user, setUser } = useContext(UserContext);
  return (
    <div>
      {user? (
        <p>User: {user.name}, Age: {user.age}</p>
      ) : (
        <p>No user logged in</p>
      )}
      <button onClick={() => setUser({ name: 'New User', age: 25 })}>
        Log in
      </button>
    </div>
  );
}

export default UserComponent;

TypeScript 会根据我们定义的类型对 usersetUser 进行类型检查,确保代码的正确性。

实际应用场景

多语言支持

在国际化应用中,我们可以使用 Context 来共享当前语言设置。不同的组件可以根据这个设置来显示相应语言的文本。

首先创建一个 LanguageContext

import React from'react';

const LanguageContext = React.createContext('en');

export default LanguageContext;

然后在 App 组件中提供当前语言:

import React from'react';
import LanguageContext from './LanguageContext';
import Header from './Header';
import Content from './Content';

function App() {
  const currentLanguage = 'zh';
  return (
    <LanguageContext.Provider value={currentLanguage}>
      <Header />
      <Content />
    </LanguageContext.Provider>
  );
}

export default App;

HeaderContent 组件中消费这个 Context 来显示相应语言的文本:

import React, { useContext } from'react';
import LanguageContext from './LanguageContext';

function Header() {
  const language = useContext(LanguageContext);
  const title = language === 'en'? 'Welcome' : '欢迎';
  return (
    <header>
      <h1>{title}</h1>
    </header>
  );
}

export default Header;
import React, { useContext } from'react';
import LanguageContext from './LanguageContext';

function Content() {
  const language = useContext(LanguageContext);
  const text = language === 'en'? 'This is the content' : '这是内容';
  return (
    <main>
      <p>{text}</p>
    </main>
  );
}

export default Content;

这样,通过改变 App 组件中 LanguageContext.Providervalue,就可以轻松切换应用的语言。

主题切换

类似于多语言支持,我们可以使用 Context 来实现主题切换。用户可以选择不同的主题,如亮色主题或暗色主题,并且应用中的各个组件会根据主题设置来显示相应的样式。

创建 ThemeContext

import React from'react';

const ThemeContext = React.createContext('light');

export default ThemeContext;

App 组件中提供主题:

import React from'react';
import ThemeContext from './ThemeContext';
import Header from './Header';
import Content from './Content';

function App() {
  const currentTheme = 'dark';
  return (
    <ThemeContext.Provider value={currentTheme}>
      <Header />
      <Content />
    </ThemeContext.Provider>
  );
}

export default App;

HeaderContent 组件中根据主题设置样式:

import React, { useContext } from'react';
import ThemeContext from './ThemeContext';

function Header() {
  const theme = useContext(ThemeContext);
  const style = theme === 'dark'? { color: 'white' } : { color: 'black' };
  return (
    <header style={style}>
      <h1>My App</h1>
    </header>
  );
}

export default Header;
import React, { useContext } from'react';
import ThemeContext from './ThemeContext';

function Content() {
  const theme = useContext(ThemeContext);
  const style = theme === 'dark'? { backgroundColor: 'black' } : { backgroundColor: 'white' };
  return (
    <main style={style}>
      <p>Some content here</p>
    </main>
  );
}

export default Content;

通过这种方式,应用中的各个组件可以轻松响应主题的变化。

与 Redux 的比较

数据管理理念

Redux 是一个用于管理应用状态的库,它遵循单向数据流的原则,所有状态集中存储在一个 store 中,组件通过 dispatch action 来修改状态。而 React Context 主要用于在组件树中共享数据,它更侧重于数据的传递,并不强制单向数据流。

使用场景

Redux 适用于大型应用中复杂状态的管理,例如电商应用中的购物车、用户订单等状态。它提供了强大的中间件机制,可以方便地进行异步操作、日志记录等。而 React Context 更适合于一些简单的共享数据场景,如主题设置、多语言切换等,不需要引入过多的状态管理逻辑。

性能方面

在性能上,Redux 通过严格的状态管理和中间件机制,在处理复杂状态变化时可以更有效地控制组件的重新渲染。而 React Context 如果使用不当,如频繁改变 Providervalue,可能会导致不必要的重新渲染。但是,如果合理使用 useMemo 等优化手段,React Context 在简单场景下也能有较好的性能表现。

例如,在一个小型的表单应用中,可能只需要使用 React Context 来共享表单的一些全局设置,如表单主题、提交按钮文本等。而在一个大型的企业级应用中,涉及到用户认证、多模块数据交互等复杂场景,Redux 可能是更好的选择。

总结 Context 在函数组件中的应用要点

  • 创建与提供 Context:通过 React.createContext 创建 Context 对象,并使用 Context.Provider 组件将数据提供给后代组件。
  • 消费 Context:在函数组件中使用 useContext Hook 来获取 Context 的值,注意多个 Context 同时使用时的处理。
  • 性能优化:使用 useMemo 避免不必要的重新渲染,创建细粒度的 Context 减少影响范围。
  • 更新机制:可以将更新函数作为 value 的一部分传递,实现手动触发 Context 更新。
  • 与 TypeScript 结合:定义 Context 值的类型,增强代码的类型安全性。
  • 实际应用:在多语言支持、主题切换等场景中发挥作用。
  • 与 Redux 比较:根据应用规模和需求选择合适的数据管理方式。

通过深入理解和合理运用 React Context 在函数组件中的应用,我们可以更高效地构建 React 应用,提升用户体验和开发效率。无论是小型项目还是大型应用,掌握 Context 的使用技巧都能为我们的开发工作带来便利。在实际开发中,需要根据具体需求权衡各种方案,以实现最优的架构设计。