在 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 状态下更新单个值

更新深层不可变状态属性时,Redux 不更新组件

Redux - 通过其键在状态数组中查找对象并更新其另一个键

Redux:使用 Redux 更新数组的状态

Reducer 状态没有被新对象更新 [redux, redux-toolkit, normalize]

Redux状态更新后没有调用ComponentDidMount?