How can I control the state of components that get added dynamically using Redux
? I'm able to do it when controlling state with React
, but when I introduced Redux
, every component has the same state.
In the example below, click the button to add a parent. Then click the button to add a new child. When clicking the add new parent button again, the second parent has the same number of children as the first. Is it possible to manage each parent state separately, knowing that there can be any number of parents, or should I stick with controlling the state with React
?
The next step would be to then print the text in all of the children with the print child info button for each parent, but since the state of the children is controlled in a similar manner to the parents, I think that'll be easy once I get over the first hurdle.
Here's the directory structure and code:
├── src
│ ├── App.jsx
│ ├── appSlice.js
│ ├── components
│ │ ├── Child.jsx
│ │ ├── Parent.jsx
│ │ ├── childSlice.js
│ │ └── parentSlice.js
│ ├── main.jsx
│ └── store
│ └── store.js
main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { Provider } from "react-redux";
import App from './App'
import { store } from "./store/store";
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
)
App.jsx
import { useDispatch, useSelector } from "react-redux"
import { addParent } from "./appSlice"
import Parent from "./components/Parent";
export default function App() {
const dispatch = useDispatch();
const {parents} = useSelector(state => state.app)
return (
<div style={{
display: 'flex',
justifyContent: 'center',
flexDirection: 'column',
margin: '2rem'
}}>
<button onClick={() => dispatch(addParent())}>Add parent</button>
<br />
{parents.map((item) => (
<Parent key={item}/>
))}
</div>
)
}
appSlice.js
import { v4 as uuid} from "uuid"
import { createSlice } from "@reduxjs/toolkit";
const appSlice = createSlice({
name: "app",
initialState: {
parents: []
},
reducers: {
addParent: (state) => {
state.parents = [...state.parents, uuid()];
},
},
});
export const { addParent } = appSlice.actions;
export default appSlice.reducer;
store/store.js
import { configureStore } from "@reduxjs/toolkit";
import appReducer from "../appSlice";
import parentReducer from "../components/parentSlice";
import childReducer from "../components/childSlice"
export const store = configureStore({
reducer: {
app: appReducer,
parent: parentReducer,
child: childReducer
},
});
components/Parent.jsx
import React from 'react'
import Child from './Child'
import { useDispatch, useSelector } from "react-redux"
import { addChild } from "./parentSlice"
export default function Parent() {
const dispatch = useDispatch()
const {children} = useSelector(state => state.parent)
return (
<div style={{
width: "100%",
height: "100px",
border: "1px solid black",
marginBottom: "10px",
}}>
<button onClick={() => dispatch(addChild())}>Add new child</button>
{children.map(item => (
<Child key={item} />
))}
<button>Print child info</button>
</div>
)
}
components/parentSlice.js
import { v4 as uuid} from "uuid"
import { createSlice } from "@reduxjs/toolkit";
const parentSlice = createSlice({
name: "parent",
initialState: {
children: []
},
reducers: {
addChild: (state) => {
state.children = [...state.children, uuid()];
},
},
});
export const { addChild } = parentSlice.actions;
export default parentSlice.reducer;
components/Child.jsx
import React from 'react'
import { useDispatch } from 'react-redux'
import { updateText } from './childSlice';
export default function Child() {
const dispatch = useDispatch();
return (
<div>
<input
type='text'
onChange={(event) => dispatch(updateText(event.target.value))}
></input>
</div>
)
}
components/childSlice.js
import { createSlice } from "@reduxjs/toolkit";
const childSlice = createSlice({
name: "child",
initialState: {
text: "",
},
reducers: {
updateText: (state, {payload}) => {
state.text = payload;
},
},
});
export const { updateText } = childSlice.actions;
export default childSlice.reducer;
In React, the UI is a function of state and props. In this case, the state is your Redux store. Basically what's missing from your code is the association of child id values to some parent id value. All the children are using the same single state.child
state value, nothing to differentiate them at all.
I suggest the following state slice changes:
appSlice
- Nothing to change here, this remains the array of parent "node" ids.
const appSlice = createSlice({
name: "app",
initialState: {
parents: []
},
reducers: {
addParent: (state) => {
state.parents = [...state.parents, uuid()];
}
}
});
parentSlice
- Here you want the state to be an object where the generated parent ids are the keys and the values are arrays of children id values.
const parentSlice = createSlice({
name: "parent",
initialState: {
children: {}
},
reducers: {
addChild: (state, action) => {
if (!state.children[action.payload]) {
state.children[action.payload] = [];
}
state.children[action.payload].push(uuid());
}
}
});
childSlice
- Similarly the child state will be an object where the keys are the child id values and the values are the input value.
const childSlice = createSlice({
name: "child",
initialState: {},
reducers: {
updateText: (state, action) => {
state[action.payload.id] = action.payload.value;
}
}
});
App
- Select state.app.parents
(or Object.keys(state.parent)
!) and map this array to the Parent
component and pass the id as a prop.
export default function App() {
const dispatch = useDispatch();
const { parents } = useSelector((state) => state.app);
return (
<div
style={{
display: "flex",
justifyContent: "center",
flexDirection: "column",
margin: "2rem"
}}
>
<button onClick={() => dispatch(addParent())}>Add parent</button>
<br />
{parents.map((id) => (
<Parent key={id} id={id} />
))}
</div>
);
}
Parent
- Similarly, take the passed id
prop and select state.parent.children[id]
to get this parent's array of children id values. Map these to Children
component and again pass the child's id value as a prop. Parent
will pass its id
value to the dispatched addChild
action so the reducer knows which parent to add a child to.
export default function Parent({ id }) {
const dispatch = useDispatch();
const children = useSelector((state) => state.parent.children[id]);
return (
<div
style={{
width: "100%",
height: "100px",
border: "1px solid black",
marginBottom: "10px"
}}
>
<button onClick={() => dispatch(addChild(id))}>Add new child</button>
{children?.map((childId) => (
<Child key={childId} id={childId} />
))}
<button>Print child info</button>
</div>
);
}
Child
- The child component will select its value via state.child[id]
using the passed id
prop and render its input. Child
will pass its id
value and the input value to the dispatched updateText
action so the reducer function knows which child id is being updated.
export default function Child({ id }) {
const dispatch = useDispatch();
const value = useSelector((state) => state.child[id]);
return (
<div>
<input
type="text"
onChange={(event) =>
dispatch(updateText({ id, value: event.target.value }))
}
value={value || ""}
/>
</div>
);
}