如何在React JS中使用useReducer维护树形组件的状态关联?
祖孙组件状态关联与useReducer实现方案
需求回顾
- 祖父组件:默认带输入框、
>按钮(打开父组件)、添加按钮(新增输入框) - 父组件:结构与祖父一致,点击
>按钮打开子组件 - 最终生成嵌套树形数据:祖父输入值作为顶级key(如
category),父组件输入值作为二级key(如agriculture),子组件输入值作为二级key对应的数组值
核心思路
把所有状态统一托管在最顶层的祖父组件,用useReducer管理状态变更逻辑。父、子组件通过props获取对应层级的状态片段和dispatch方法,触发状态更新,保证数据关联的一致性。
1. 定义状态结构
设计匹配最终输出的状态格式,初始状态示例:
const initialState = { rootItems: [ // 每个祖父项:key是输入值,children是父组件的项列表 { key: "", children: [] } ] }; // 父组件项的结构 // { key: "", values: [""] } // values是子组件的输入值列表
2. 编写Reducer函数
处理所有状态变更动作,覆盖添加输入项、更新输入值、添加子层级等场景:
function treeReducer(state, action) { switch (action.type) { // 更新祖父项的key case "UPDATE_ROOT_KEY": { const { index, value } = action.payload; const newRootItems = [...state.rootItems]; newRootItems[index] = { ...newRootItems[index], key: value }; return { ...state, rootItems: newRootItems }; } // 添加新的祖父输入项 case "ADD_ROOT_ITEM": { return { ...state, rootItems: [...state.rootItems, { key: "", children: [] }] }; } // 更新父组件项的key case "UPDATE_PARENT_KEY": { const { rootIndex, parentIndex, value } = action.payload; const newRootItems = [...state.rootItems]; const parentItems = [...newRootItems[rootIndex].children]; parentItems[parentIndex] = { ...parentItems[parentIndex], key: value }; newRootItems[rootIndex] = { ...newRootItems[rootIndex], children: parentItems }; return { ...state, rootItems: newRootItems }; } // 添加新的父组件项 case "ADD_PARENT_ITEM": { const { rootIndex } = action.payload; const newRootItems = [...state.rootItems]; newRootItems[rootIndex] = { ...newRootItems[rootIndex], children: [...newRootItems[rootIndex].children, { key: "", values: [""] }] }; return { ...state, rootItems: newRootItems }; } // 更新子组件的输入值 case "UPDATE_CHILD_VALUE": { const { rootIndex, parentIndex, childIndex, value } = action.payload; const newRootItems = [...state.rootItems]; const parentItems = [...newRootItems[rootIndex].children]; const childValues = [...parentItems[parentIndex].values]; childValues[childIndex] = value; parentItems[parentIndex] = { ...parentItems[parentIndex], values: childValues }; newRootItems[rootIndex] = { ...newRootItems[rootIndex], children: parentItems }; return { ...state, rootItems: newRootItems }; } // 添加新的子组件输入项 case "ADD_CHILD_ITEM": { const { rootIndex, parentIndex } = action.payload; const newRootItems = [...state.rootItems]; const parentItems = [...newRootItems[rootIndex].children]; parentItems[parentIndex] = { ...parentItems[parentIndex], values: [...parentItems[parentIndex].values, ""] }; newRootItems[rootIndex] = { ...newRootItems[rootIndex], children: parentItems }; return { ...state, rootItems: newRootItems }; } default: return state; } }
3. 拆分组件实现
祖父组件(Grandparent)
持有全局状态,渲染自身输入项,传递dispatch和对应状态给父组件:
import { useReducer } from "react"; import ParentComponent from "./ParentComponent"; function GrandparentComponent() { const [state, dispatch] = useReducer(treeReducer, initialState); // 将状态转换为最终需要的格式 const getFinalStructure = () => { return state.rootItems.reduce((acc, item) => { if (!item.key) return acc; acc[item.key] = item.children.reduce((childAcc, childItem) => { if (!childItem.key) return childAcc; childAcc[childItem.key] = childItem.values.filter(val => val); // 过滤空值 return childAcc; }, {}); return acc; }, {}); }; return ( <div> {state.rootItems.map((rootItem, rootIndex) => ( <div key={`root-${rootIndex}`} style={{ margin: "10px 0", padding: "10px", border: "1px solid #ccc" }}> <input value={rootItem.key} onChange={(e) => dispatch({ type: "UPDATE_ROOT_KEY", payload: { index: rootIndex, value: e.target.value } })} placeholder="输入顶级分类" /> <button onClick={() => dispatch({ type: "ADD_PARENT_ITEM", payload: { rootIndex } })}> > </button> {/* 渲染该祖父项对应的父组件列表 */} {rootItem.children.map((parentItem, parentIndex) => ( <ParentComponent key={`parent-${rootIndex}-${parentIndex}`} rootIndex={rootIndex} parentIndex={parentIndex} parentItem={parentItem} dispatch={dispatch} /> ))} </div> ))} <button onClick={() => dispatch({ type: "ADD_ROOT_ITEM" })}>添加顶级项</button> {/* 查看最终结构 */} <pre>{JSON.stringify(getFinalStructure(), null, 2)}</pre> </div> ); } export default GrandparentComponent;
父组件(Parent)
渲染自身输入项,传递dispatch和对应状态给子组件:
import ChildInputComponent from "./ChildInputComponent"; function ParentComponent({ rootIndex, parentIndex, parentItem, dispatch }) { return ( <div style={{ marginLeft: "20px", margin: "10px 0", padding: "10px", border: "1px solid #eee" }}> <input value={parentItem.key} onChange={(e) => dispatch({ type: "UPDATE_PARENT_KEY", payload: { rootIndex, parentIndex, value: e.target.value } })} placeholder="输入二级分类" /> <button onClick={() => dispatch({ type: "ADD_CHILD_ITEM", payload: { rootIndex, parentIndex } })}> > </button> {/* 渲染该父项对应的子输入框列表 */} {parentItem.values.map((value, childIndex) => ( <ChildInputComponent key={`child-${rootIndex}-${parentIndex}-${childIndex}`} rootIndex={rootIndex} parentIndex={parentIndex} childIndex={childIndex} value={value} dispatch={dispatch} /> ))} </div> ); } export default ParentComponent;
子组件(ChildInput)
渲染输入框,处理值更新:
function ChildInputComponent({ rootIndex, parentIndex, childIndex, value, dispatch }) { return ( <div style={{ marginLeft: "40px", margin: "5px 0" }}> <input value={value} onChange={(e) => dispatch({ type: "UPDATE_CHILD_VALUE", payload: { rootIndex, parentIndex, childIndex, value: e.target.value } })} placeholder="输入值" /> </div> ); } export default ChildInputComponent;
方案优势
- 状态集中管理,所有数据变更都通过
dispatch触发,避免组件间状态不一致 - 组件职责清晰:祖父管全局状态,父组件管二级层级,子组件管具体值输入
- 容易扩展:后续要加更多层级,只需要在reducer中新增对应动作,新增组件即可
内容的提问来源于stack exchange,提问作者Jaskirat Singh
相关产品推荐
相关产品推荐

