在 Redux 中更新递归对象状态
Posted
技术标签:
【中文标题】在 Redux 中更新递归对象状态【英文标题】:Updating Recursive Object state in Redux 【发布时间】:2021-06-26 02:04:53 【问题描述】:我想更新存储在 redux 状态的子对象中的“名称”。
目前,我正在使用 redux 工具包并以 redux 状态存储“TElement”数据(来自 api)。 TElement 具有递归数据结构。 我能够绘制出 React 中的所有子组件。但是,我不知道如何更新 TElement 元素的状态。
createSlice.ts
export interface TElement
id: string;
name: string;
link: string;
elements: TElement[];
;
const initalState: TElements =
TElement:
id: '',
name: '',
link: '',
elements: []
const systemSlice = createSlice(
name: 'system',
initialState: initialState as TElements,
reducers:
)
export const root = (state: RootState): TElements['TElement'] =>
state.system.TElement;
组件.tsx '希望更新输入字段中的名称'
const File: React.FC<TElement> = (
id,
name,
link,
elements,
: TElement) =>
const [showChildren, setShowChildren] = useState<boolean>(false);
const handleClick = useCallback(() =>
setShowChildren(!showChildren);
, [showChildren, setShowChildren]);
return (
<div>
<input
onClick=handleClick
style= fontWeight: showChildren ? 'bold' : 'normal' >
name
</input>
<div
style=
position: 'relative',
display: 'flex',
flexDirection: 'column',
left: 25,
borderLeft: '1px solid',
paddingLeft: 15,
>
showChildren &&
(child ?? []).map((node: FileNode) => <File key=id ...node />)
</div>
</div>
)
function TaskFilter(): JSX.Element
const root = useSelector(root);
return (
<div>
<File ...root />
</div>
);
export default TaskFilter;
【问题讨论】:
【参考方案1】:要了解递归,您必须了解递归。这是一个递归渲染的示例,并在操作中将所有父 id 提供给更新,以便化简器可以递归更新。
const Provider, useDispatch, useSelector = ReactRedux;
const createStore, applyMiddleware, compose = Redux;
const initialState =
elements: [
id: '1',
name: 'one',
elements: [
id: '2',
name: 'two',
elements: [
id: '3',
name: 'three',
elements: [],
,
],
,
],
,
id: '4',
name: 'four',
elements: [],
,
],
;
//action types
const NAME_CHANGED = 'NAME_CHANGED';
//action creators
const nameChanged = (parentIds, id, newName) => (
type: NAME_CHANGED,
payload: parentIds, id, newName ,
);
//recursive update for reducer
const recursiveUpdate = (
elements,
parentIds,
id,
newName
) =>
const recur = (elements, parentIds, id, newName) =>
//if no more parent ids
if (parentIds.length === 0)
return elements.map((element) =>
element.id === id
? ...element, name: newName
: element
);
const currentParent = parentIds[0];
//recursively update minus current parent id
return elements.map((element) =>
element.id === currentParent
?
...element,
elements: recursiveUpdate(
element.elements,
parentIds.slice(1),
id,
newName
),
: element
);
;
return recur(elements, parentIds, id, newName);
;
const reducer = (state, type, payload ) =>
if (type === NAME_CHANGED)
const parentIds, id, newName = payload;
return
...state,
elements: recursiveUpdate(
state.elements,
parentIds,
id,
newName
),
;
return state;
;
//selectors
const selectElements = (state) => state.elements;
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(() => (next) => (action) =>
next(action)
)
)
);
//Element will recursively call itself
const Element = React.memo(function ElementComponent(
parentIds,
element,
)
const dispatch = useDispatch();
const onNameChange = (e) =>
dispatch(
nameChanged(parentIds, element.id, e.target.value)
);
const id = element;
console.log('render', id);
//make parentIds array for children, use memo to not needlessly
// re render all elements on name change
const childParentIds = React.useMemo(
() => parentIds.concat(id),
[parentIds, id]
);
return (
<li>
<input
type="text"
value=element.name
onChange=onNameChange
/>
/* SO does not support optional chaining but you can use
Boolean(element.elements?.length) instead */
Boolean(
element.elements && element.elements.length
) && (
<ul>
element.elements.map((child) => (
// recursively render child elements
<Element
key=child.id
element=child
parentIds=childParentIds
/>
))
</ul>
)
</li>
);
);
const App = () =>
const elements = useSelector(selectElements);
const parentIds = React.useMemo(() => [], []);
return (
<ul>
elements.map((element) => (
<Element
key=element.id
parentIds=parentIds
element=element
/>
))
</ul>
);
;
ReactDOM.render(
<Provider store=store>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<div id="root"></div>
【讨论】:
【参考方案2】:我的建议是将它们存储在扁平结构中。这使得存储它们变得更加困难(如果它们来自嵌套结构中的 API),但更容易更新它们。
您将存储一个以id
为键的元素字典,以便您可以轻松查找和更新元素。您可以将递归的 element
属性替换为直接子代的 childIds
数组。
export interface TElement
id: string;
name: string;
link: string;
elements: TElement[];
export type StoredElement = Omit<TElement, "elements"> &
childIds: string[];
;
您的切片可能如下所示:
export const elementAdapter = createEntityAdapter<StoredElement>();
const flatten = (
element: TElement,
dictionary: Record<string, StoredElement> =
): Record<string, StoredElement> =>
const elements, ...rest = element;
dictionary[element.id] = ...rest, childIds: elements.map((e) => e.id) ;
elements.forEach((e) => flatten(e, dictionary));
return dictionary;
;
const systemSlice = createSlice(
name: "system",
initialState: elementAdapter.getInitialState(
rootId: "" // id of the root element
),
reducers:
receiveOne: (state, payload : PayloadAction<TElement>) =>
elementAdapter.upsertMany(state, flatten(payload));
,
receiveMany: (state, payload : PayloadAction<TElement[]>) =>
payload.forEach((element) =>
elementAdapter.upsertMany(state, flatten(element))
);
,
rename: (
state,
payload : PayloadAction<Pick<TElement, "id" | "name">>
) =>
const id, name = payload;
elementAdapter.updateOne(state, id, changes: name );
);
export const receiveOne, receiveMany, rename = systemSlice.actions;
export default systemSlice.reducer;
还有商店:
const store = configureStore(
reducer:
system: systemSlice.reducer
);
type RootState = ReturnType<typeof store.getState>;
type AppDispatch = typeof store.dispatch;
export const useSelector = createSelectorHook<RootState>();
const selectById = elementAdapter.getSelectors(
(state: RootState) => state.system
);
还有你的组件:
const RenderFile: React.FC<StoredElement> = ( id, name, link, childIds ) =>
const dispatch = useDispatch();
const [showChildren, setShowChildren] = useState(false);
const handleClick = useCallback(() =>
setShowChildren((prev) => !prev);
, [setShowChildren]);
const [text, setText] = useState(name);
const onSubmitName = () =>
dispatch(rename( id, name: text ));
;
return (
<div>
<div>
<label>
Name:
<input
type="text"
value=text
onChange=(e) => setText(e.target.value)
/>
</label>
<button onClick=onSubmitName>Submit</button>
</div>
<div>
<div onClick=handleClick>
Click to showChildren ? "Hide" : "Show" Children
</div>
showChildren && childIds.map((id) => <FileById key=id id=id />)
</div>
</div>
);
;
const FileById: React.FC< id: string > = ( id ) =>
const file = useSelector((state) => selectById(state, id));
if (!file)
return null;
return <RenderFile ...file />;
;
const TaskFilter = () =>
const rootId = useSelector((state) => state.system.rootId);
return (
<div>
<FileById id=rootId />
</div>
);
;
export default TaskFilter;
Code Sandbox Link
【讨论】:
以上是关于在 Redux 中更新递归对象状态的主要内容,如果未能解决你的问题,请参考以下文章
Redux - 通过其键在状态数组中查找对象并更新其另一个键