Skip to content

受控/非受控双模

本章前置知识:需要了解受控组件和非受控组件的区别

1.复合组件进阶

基于符合组件的模式,我们可以实现一个简单的菜单组件。

jsx
function App() {
  return (
    <Menu>
      {/* 这里的 name 对应状态值 */}
      <Menu.Item name="home">首页</Menu.Item>
      <Menu.Item name="settings">设置</Menu.Item>
      <Menu.Item name="profile">个人中心</Menu.Item>
    </Menu>
  );

2.如何在拿到当前选中的项?

3.状态提升

“React”的标准做法是:状态提升 (Lifting State Up),也就是把 Menu 变成一个 “受控组件” (Controlled Component)。

这就好比原生 <input> 标签

  • 非受控:<input /> (自己管理内容)
  • 受控:<input value={text} onChange={setText} /> (父组件管理内容) 我们需要把你的 Menu 改造得像 <input> 一样。

我们需要把内部的 useState 删掉,改成读取 Props

jsx
// 
import React, { createContext, useState, useContext } from 'react';

const MenuContext = createContext();

// 注意这里接收了两个新 Props: activeKey (当前选中值) 和 onChange (回调)
function Menu({ children, activeKey, onChange }) {
  
  // ❌ 删掉这一行,不再自己管理状态
  // const [activeItem, setActiveItem] = useState("");

  // ✅ 构造 Context 数据
  // activeItem 直接读取 props 里的 activeKey
  // setActiveItem 直接使用 props 里的 onChange
  const contextValue = { 
    activeItem: activeKey, 
    setActiveItem: onChange 
  };

  return (
    <MenuContext.Provider value={contextValue}>
      <div className="menu-container">
        {children}
      </div>
    </MenuContext.Provider>
  );
}

// ... Item 组件的代码完全不用动!保持原样 ...
function Item({ children, name }) {
   const { activeItem, setActiveItem } = useContext(MenuContext);
   // ...略
}

Menu.Item = Item;
export default Menu;

现在 App ,拥有控制权。

jsx
import React, { useState } from 'react';
import Menu from './Menu';

function App() {
  // 1. 状态在 App 里管理
  const [currentTab, setCurrentTab] = useState("home");

  return (
    <div>
      {/* 2. 把状态和修改状态的方法传给 Menu */}
      <Menu 
        activeKey={currentTab} 
        onChange={(key) => setCurrentTab(key)} 
      >
        <Menu.Item name="home">首页</Menu.Item>
        <Menu.Item name="settings">设置</Menu.Item>
        <Menu.Item name="profile">个人中心</Menu.Item>
      </Menu>

      {/* 3. 现在 App 可以根据状态渲染不同的内容了! */}
      <div className="content-area" style={{ marginTop: '20px', padding: '10px', border: '1px dashed grey' }}>
        <h3>当前页面内容:</h3>
        {currentTab === 'home' && <p>🏠 欢迎来到首页!这里有很多新闻...</p>}
        {currentTab === 'settings' && <p>⚙️ 这里是设置页面,请调节你的偏好。</p>}
        {currentTab === 'profile' && <p>👤 这里的个人资料,你好!</p>}
      </div>
    </div>
  );
}

4.进阶 ( 组件库级封装 )

如果我希望这个 Menu 既能像刚才那样受控(传 props),也能像最开始那样非受控(自己管自己,省事),该怎么办?

市面上的组件库通过一个自定义 Hook (通常叫 useControllableValue) 来实现这种“双模”切换:

  • 如果用户传了 value,就用用户的。
  • 如果用户没传,就用内部的 useState。

双模式

  • 核心逻辑
    • 看父组件有没有传 activeKey (或 value) 这个 prop 进来。
    • 如果传了 (!== undefined):说明是受控模式,忽略内部 state,完全依赖 props
    • 没传:说明是非受控模式,使用内部 useState。

为了代码复用,通常我们会写一个自定义 Hook,叫 useControllableValue

jsx
// 工具函数
import { useState, useCallback } from 'react';

function useControllableValue(props = {}) {
  // 1. 结构出关键参数
  // value: 受控值 (父组件传进来的)
  // defaultValue: 默认值 (非受控模式下的初始值)
  // onChange: 变更时的回调
  const { value, defaultValue, onChange } = props;
  // 2. 判断是否受控:只要 value 不是 undefined,就是受控模式
  const isControlled = value !== undefined;
  // 3. 定义内部状态 (只在非受控模式下有用)
  // 如果是受控模式,这个 internalState 其实就没用了,但 React 需要我们定义它
  const [internalState, setInternalState] = useState(defaultValue);
  // 4. 决定最终用哪个值:受控用 value,非受控用 internalState
  const finalValue = isControlled ? value : internalState;
  // 5. 定义触发变更的函数
  const triggerChange = useCallback((newValue) => {
    // 如果是非受控模式,我们需要更新内部状态,界面才会变
    if (!isControlled) {
      setInternalState(newValue);
    }
    // 无论哪种模式,都要通知父组件 "我想改值"
    if (onChange) {
      onChange(newValue);
    }
  }, [isControlled, onChange]);
  return [finalValue, triggerChange];
}

升级 Menu 组件

jsx
import React, { createContext, useContext } from 'react';
// 引入上面写的 Hook (假设在同一个文件或 import 进来)
// import { useControllableValue } from './hooks'; 

const MenuContext = createContext();

function Menu({ children, activeKey, defaultActiveKey, onChange }) {
  // 用 Hook 处理双模式逻辑:一行代码搞定双模逻辑
  const [currentValue, setCurrentValue] = useControllableValue({
    value: activeKey,          // 受控属性名
    defaultValue: defaultActiveKey, // 非受控属性名
    onChange: onChange         // 回调
  });

  const contextValue = { 
    activeItem: currentValue, 
    setActiveItem: setCurrentValue 
  };

  return (
    <MenuContext.Provider value={contextValue}>
      <div className="menu-container">
        {children}
      </div>
    </MenuContext.Provider>
  );
}

// Item 代码依然不用变!
Menu.Item = ({ children, name }) => {
  const { activeItem, setActiveItem } = useContext(MenuContext);
  const isActive = activeItem === name;
  return (
    <div 
      onClick={() => setActiveItem(name)}
      style={{ color: isActive ? 'red' : 'black', cursor: 'pointer' }}
    >
      {children}
    </div>
  );
};

export default Menu;

使用场景

  1. 偷懒模式 (非受控)
jsx
// ✅ 简单好用
<Menu defaultActiveKey="home">
  <Menu.Item name="home">首页</Menu.Item>
  <Menu.Item name="settings">设置</Menu.Item>
</Menu>
  1. 掌控模式 (受控)
jsx
// ✅ 高级控制
function App() {
  const [tab, setTab] = useState('home');
  
  return (
    <div>
      <button onClick={() => setTab('home')}>强制回到首页</button>
      
      <Menu 
        activeKey={tab} 
        onChange={(key) => {
          console.log("用户想切换到:", key);
          setTab(key);
        }}
      >
        <Menu.Item name="home">首页</Menu.Item>
        <Menu.Item name="settings">设置</Menu.Item>
      </Menu>
    </div>
  );
}