Reputation: 121
I want to update the 'name' stored in child object of redux state.
Currently, I am using redux toolkit and storing, 'TElement' data (from api) in redux state. TElement has recursice data structure. I was able to map out all the child components in React. However, I don't know how to go about updating the state of TElement's elements.
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;
Component.tsx 'Wish to update name in input field'
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;
Upvotes: 1
Views: 1338
Reputation: 39290
To understand recursion you have to understand recursion. Here is an example that will recursively render and in the action provide all the parent ids to the update so the reducer can recursively update.
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>
Upvotes: 1
Reputation: 42228
My recommendation would be to store them in a flat structure. This makes it more difficult to store them (if they are coming from the API in a nested structure), but much easier to update them.
You would store a dictionary of elements keyed by their id
so that you can look up and update an element easily. You would replace the recursive element
property with an array of the childIds
of the direct children.
export interface TElement {
id: string;
name: string;
link: string;
elements: TElement[];
}
export type StoredElement = Omit<TElement, "elements"> & {
childIds: string[];
};
Here's what your slice might look like:
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;
And the store:
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
);
And your components:
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;
Upvotes: 3