受控/非受控双模
本章前置知识:需要了解受控组件和非受控组件的区别
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;使用场景
- 偷懒模式 (非受控)
jsx
// ✅ 简单好用
<Menu defaultActiveKey="home">
<Menu.Item name="home">首页</Menu.Item>
<Menu.Item name="settings">设置</Menu.Item>
</Menu>- 掌控模式 (受控)
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>
);
}