I'm trying to understand how make a component that can remove itself from a array of components with functional components. Here is the sample code of what I'm trying to do:
const App = () => {
<ObjState>
<ObjectCreator />
<ObjectList />
</ObjState>
}
const ObjContext = createContext();
const ObjReducer = (state, { type, payload }) => {
switch(type) {
case Types.ADD_OBJ:
return {
...state,
objects: [...state.objects, payload]
};
case Types.REMOVE_OBJ:
return {
...state,
objects: state.objects.filter(obj => obj !== payload)
};
default:
return state;
}
}
const ObjState = ({ children }) => {
const initialState = {
objects: []
}
const [state, dispatch] = useReducer(ObjRecuder, initialState);
const addObj = (obj) => {
dispatch({
type: Types.ADD_OBJ,
payload: obj
});
}
const removeObj = (obj) => {
dispatch({
type: Types.REMOVE_OBJ,
payload: obj
});
}
return (
<ObjContext.Provider value={{
objects: state.objects,
addObj,
removeObj
}}>
{children}
</ObjContext.Provider>
);
}
const ObjCreator = () => {
const { addObject } = useContext(ObjContext);
const createObj =() => {
const obj = (<ObjectTypeA key={uuid()} />);
addObject(obj);
}
return (<button onClick={createObj}>create an object!</button>)
}
const ObjectList = () => {
const { objects } = useContext(ObjContext)
return (
<fragment>
{objects}
</fragment>
)
}
const ObjectTypeA = ({ key }) => {
const { removeObj } = useContext(ObjContext);
const removeSelf = () => {
removeObj(this);
}
return (
<button onClick={removeSelf}>remove me!</button>
)
}
The problem is you can't reference this in the final Object component.
I have the unique key but I'm not sure how to pass it through correctly. I attempted to build a reducer action that took the key from the Object and removed it that way but key came back as undefined even though it is deconstructed out of the props and I'm using an arrow function to preserve it.
I feel like I'm tackling this problem in the wrong way.
Issue
I think you veer off-course when trying to store what looks to be React components in your context state, you should be storing objects instead. The objects should have unique GUIDs. This allows the reducer to identify which object element to remove from state. The ObjectList should then render derived React components from the stored state.
I attempted to build a reducer action that took the key from the
Object and removed it that way but key came back as undefined even
though it is deconstructed out of the props and I'm using an arrow
function to preserve it.
This is because React keys (and refs) are not actually props. Keys can't be accessed in children components. You can can pass the same value via just about any other named prop though. Note below in solution I pass a React key and an id prop.
Solution
ObjectCreator: Creates objects, not React components
const ObjectCreator = () => {
const { addObj } = useContext(ObjContext);
const createObj = () => {
const obj = {
id: uuid()
};
addObj(obj);
};
return <button onClick={createObj}>create an object!</button>;
};
SpecificObject: passes its id to the removeObj callback.
const MyObject = ({ id }) => {
const { removeObj } = useContext(ObjContext);
const removeSelf = () => {
removeObj(id);
};
return (
<div>
<button onClick={removeSelf}>remove {id}</button>
</div>
);
};
ObjectList: renders the context objects mapped to JSX.
const ObjectList = () => {
const { objects } = useContext(ObjContext);
return (
<>
{objects.map((el) => (
<MyObject key={el.id} id={el.id} />
))}
</>
);
};
Check the passed id payload in the remove object reducer
const ObjReducer = (state, { type, payload }) => {
switch (type) {
case Types.ADD_OBJ:
return {
...state,
objects: [...state.objects, payload]
};
case Types.REMOVE_OBJ:
return {
...state,
objects: state.objects.filter((obj) => obj.id !== payload)
};
default:
return state;
}
};
Demo
Related
Is there a way to select a derived array from an array in a Redux store without spurious renders?
My Redux store contains an array of objects.
state = {items: [{id: 1, keys...}, {id: 2, keys...}, {id: 3, keys...}, ...]}
I wrote a selector to return an array of ids.
const selectIds = (state: MyStateType) => {
const {items} = state;
let result = [];
for (let i = 0; i < items.length; i++) {
result.push(items[I].id);
}
return result;
};
I then call this selector using react-redux's useSelector hook, inside a component to render out a list of components.
const MyComponent = () => {
const ids = useSelector(selectIds);
return (
<>
{ids.map((id) => (
<IdComponent id={id} key={id} />
))}
</>
);
};
I am finding that MyComponent is being rendered every call to dispatch which breaks down performance at a higher number of array elements.
I have passed in an equality function to useSelector like so:
import {shallowEqual, useSelector } from "react-redux";
const ids = useSelector(selectIds, (a, b) => {
if (shallowEqual(a, b)) {
return true;
}
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (a[i].id !== b[i].id) {
return false;
}
}
return true;
});
But dispatch is called enough times that checking equality becomes expensive with a large amount of array elements.
I have tried using the reselect library as well.
const selectItems = (state: MyStateType) => {
return state.items;
};
const selectIds = createSelector(
selectItems,
(items) => {
let result = [];
for (let i = 0; i < items.length; i++) {
result.push(items[i].id);
}
return result;
}
);
However, every time I modify the properties of one array element in state.items via dispatch, this changes the dependency of selectItems which causes selectIds to recalculate.
What I want is for selectIds to only recompute when the ids of state.items are modified. Is this possible?
I think the best you can do here is to combine reselect with the use of shallowEqual:
import { shallowEqual } from "react-redux";
const selectItems = (state: MyStateType) => state.items;
const selectIds = createSelector(
selectItems,
(items) => items.map(item => item.id)
);
const MyComponent = () => {
const ids = useSelector(selectIds, shallowEqual);
return (
<>
{ids.map((id) => (
<IdComponent id={id} key={id} />
))}
</>
);
};
Notes
I'm using Array.map to extract ids: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map
shallowEqual can be passed directly to useSelector
With the code above:
The array of ids will be re-created only if state.items change.
The ids variable will have a new reference only if the ids changed.
If this solution is not enough (can't afford the shallowEqual) you can take a look at https://github.com/dai-shi/react-tracked it uses a more precise system to track which part of the state is used (using Proxies: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy).
Another way of doing this is to memoize the ids array in the selector:
const { createSelector, defaultMemoize } = Reselect;
const selectItems = (state) => {
return state.items;
};
const selectIds = (() => {
//memoize the array
const memArray = defaultMemoize((...ids) => ids);
return createSelector(selectItems, (items) =>
memArray(...items.map(({ id }) => id))
);
})(); //IIFE
//test the code:
const state = {
items: [{ id: 1 }, { id: 2 }],
};
const result1 = selectIds(state);
const newState = {
...state,
items: state.items.map((item) => ({
...item,
newValue: 88,
})),
};
const result2 = selectIds(newState);
console.log('are they the same:', result1 === result2);
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
I am dynamically adding <div> elements to a component by adding them to an array. This is not a problem and works well. The issue I'm trying to solve here is removing the <div> on double click by passing the id of the <div> that was doubled clicked with props when the reducer is dispatched.
The main issue is the array filter function only works when I code hard the div id both on the div and in the filter function when I want to pass the id of e.target.id on dispatch of delDiv reducer.
Note: I can remove the div successfully by changing the addDivReducer like this:
case "ADD_DIV":
return state.concat(
<DivComponent
key={Math.floor(Math.random() * 100) + 1}
id={11} ***************************************************** Changed
/>
);
case "DELETE_DIV":
state = state.filter((elements) => {
return elements.props.id !== 11; *********************************** Changed
});
return state;
But the desired effect is to pass id as props on dispatch as seen in my code below
The reducer that adds a removes elements look like this:
import DivComponent from "../../components/AddDivComponent";
const addDivReducer = (state, action) => {
switch (action.type) {
case "ADD_DIV":
return state.concat(
<DivComponent
key={Math.floor(Math.random() * 100) + 1}
id={Math.floor(Math.random() * 100) + 1}
/>
);
case "DELETE_DIV":
state = state.filter((elements) => {
return elements.props.id !== action.payload;
});
return state;
default:
return (state = []);
}
};
export default addClipartReducer;
The actions index.js look like:
export const addDiv = (props) => {
return {
type: "ADD_DIV",
payload: props,
};
};
export const deleteDiv = (props) => {
return {
type: "DELETE_DIV",
payload: props,
};
};
The delete reducer is being dispatched when the div is double clicked on like this in AddDivComponent.js:
import { useDispatch } from "react-redux";
import { deleteDiv } from "../../store/actions";
const AddDivComponent = (props) => {
const dispatch = useDispatch();
const removeClipart = (e) => {
dispatch(deleteDiv(e.target.id));
};
return(
<div
id={props.id}
className="my-div"
onDoubleClick={removeDiv}
/>
);
};
export default DivComponent;
Finally the array of <div> elements is being shown here in Canvas.js:
import { useSelector } from "react-redux";
const Canvas = () => {
const divList = useSelector((state) => state.addDIV);
return(
<div className="canvas">
{divList}
</div>
);
};
export default Canvas;
you are mutating state at your DELETE_DIV reducer. If you need to handle state, create a copy a first:
// mutating state here to a new value, can lead to problems
state = state.filter((elements) => {
return elements.props.id !== action.payload;
});
I would suggest to return filter directly, given filter already returns the desired next state, while not mutating the original:
case "DELETE_DIV":
return state.filter((elements) => {
return elements.props.id !== action.payload;
});
I have a very weird bug that I'm trying to understand for 1.5 days now. The problem with this bug is, that it is very hard to show it without showing around 2000 lines of code - I tried rebuilding a simple example in a codesandbox but couldn't reproduce the bug.
The bug can be easily described, though:
I have a parent component A, and a child component B. Both are connected to the same redux store and subscribed to a reducer called active. Both components print the exact same activeQuestion state property. Both components are connected to the redux store individually via connect()
I dispatch an action SET_ACTIVE_QUESTION and the components rerender (I'm not sure why each re-render happens) and component B now has the updated state from the store and component A doesn't ... and I can't seem to figure out why that is.
The real application is fairly big but there are a couple of weird things that I observed:
The bug disappears when I subscribe the parent component of A to the active state (Component A is subscribed itself).
The action to change the active question is qued before it is fired with setTimeout(() => doAction(), 0). If I remove the setTimeout the bug disappears.
Here is why I think this question is relevant even without code: How is it even possible that an action is dispatched in the redux store (the first console log is directly from the reducer) and the wrong state is displayed on a subsequent render? I'm not sure how this could even be possible unless its a closure or something.
Update (mapStateToProps) functions:
Component A (wrong state):
const mapStateToProps = (state: AppState) => ({
active: state.active,
answerList: state.answerList,
surveyNotifications: state.surveyNotifications,
activeDependencies: state.activeDependencies,
});
Component B (right state):
const mapStateToProps = (state: AppState) => ({
surveyNotifications: state.surveyNotifications,
active: state.active,
answerList: state.answerList,
activeDependencies: state.activeDependencies,
});
Update:
The state transition is triggered by component B (correct state) with this function:
const goToNextQuestionWithTransition = (
where: string,
shouldPerformValidation?: boolean
) => {
setInState(false);
setTimeout(() => {
props.goToQuestion(where, shouldPerformValidation);
}, 200);
};
Removing the setTimeout removes the bug (but I don't know why)
Update (show reducer):
export const INITIAL_SATE = {
activeQuestionUUID: '',
...
};
export default function (state = INITIAL_SATE, action) {
switch (action.type) {
case actionTypes.SET_ACTIVE_QUESTION:
console.log('Action from reducer', action)
return { ...state, activeQuestionUUID: action.payload };
...
default:
return {...state};
}
}
Update
Component A - correct state
const Survey: React.FC<IProps> = (props) => {
const {
survey,
survey: { tenantModuleSet },
} = props;
const [isComplete, setIsComplete] = React.useState(false);
const classes = useStyles();
const surveyUtils = useSurveyUtils();
console.log('Log from component A', props.active.activeQuestionUUID)
React.useEffect(() => {
const firstModule = tenantModuleSet[0];
if (firstModule) {
props.setActiveModule(firstModule.uuid);
} else {
setIsComplete(true);
}
}, []);
const orderedLists: IOrderedLists = useMemo(() => {
let orderedQuestionList: Array<string> = [];
let orderedModuleList: Array<string> = [];
tenantModuleSet.forEach((module) => {
orderedModuleList.push(module.uuid);
module.tenantQuestionSet.forEach((question) => {
orderedQuestionList.push(question.uuid);
});
});
return {
questions: orderedQuestionList,
modules: orderedModuleList,
};
}, [survey]);
const validateQuestion = (question: IQuestion) => {
...
};
const findModuleForQuestion = (questionUUID: string) => {
...
};
const { setActiveQuestion, setActiveModule, active } = props;
const { activeQuestionUUID, activeModuleUUID } = props.active;
const currentQuestionIndex = orderedLists.questions.indexOf(
activeQuestionUUID
);
const currentModuleIndex = orderedLists.modules.indexOf(activeModuleUUID);
const currentModule = props.survey.tenantModuleSet.filter(
(module) => module.uuid === active.activeModuleUUID
)[0];
if (!currentModule) return null;
const currentQuestion = currentModule.tenantQuestionSet.filter(
(question) => question.uuid === activeQuestionUUID
)[0];
const handleActiveSurveyScrollDirection = (destination: string) => {
...
};
const isQuestionLastInModule = ...
const moveToNextQuestion = (modules: string[], questions: string[]) => {
if (isQuestionLastInModule) {
if (currentModule.uuid === modules[modules.length - 1]) {
props.setActiveSurveyView("form");
} else {
setActiveQuestion("");
setActiveModule(modules[currentModuleIndex + 1]);
}
} else {
console.log('this is the move function')
setActiveQuestion(questions[currentQuestionIndex + 1]);
}
};
const goToQuestiton = (destination: string, useValidation = true) => {
....
moveToNextQuestion(modules, questions);
};
return (
<section className={classes.view}>
{isComplete ? (
<SurveyComplete />
) : (
<div className={classes.bodySection}>
<Module
// adding a key here is nessesary
// or the Module will not unmount when the module changes
key={currentModule.uuid}
module={currentModule}
survey={props.survey}
goToQuestion={goToQuestiton}
/>
</div>
)}
{!isComplete && (
<div className={classes.footerSection}>
<SurveyFooter
tenantModuleSet={props.survey.tenantModuleSet}
goToQuestion={goToQuestiton}
orderedLists={orderedLists}
/>
</div>
)}
</section>
);
};
const mapStateToProps = (state: AppState) => ({
active: state.active,
answerList: state.answerList,
surveyNotifications: state.surveyNotifications,
activeDependencies: state.activeDependencies,
});
const mapDispatchToProps = (dispatch: Dispatch) =>
bindActionCreators(
{
removeQuestionNotification,
setActiveQuestion,
setActiveModule,
setActiveSurveyScrollDirection,
},
dispatch
);
export default connect(mapStateToProps, mapDispatchToProps)(Survey);
Component B (wrong state)
const Question: React.FC<IProps> = (props: IProps) => {
const [showSubmitButton, setShowSubmitButton] = React.useState(false);
const [inState, setInState] = React.useState(true);
const classes = useStyles();
const { question, module, goToQuestion, active } = props;
const notifications: Array<IQuestionNotification> =
props.surveyNotifications[question.uuid] || [];
const answerArr = props.answerList[question.uuid];
const dependency = props.activeDependencies.questions[question.uuid];
useEffect(() => {
/**
* Function that moves to next or previous question based on the activeSurveyScrollDirection
*/
const move =
active.activeSurveyScrollDirection === "forwards"
? () => goToQuestion("next", false)
: () => goToQuestion("prev", false); // backwards
if (!dependency) {
if (!question.isVisible) move();
} else {
const { type } = dependency;
if (type === DependencyTypeEnum.SUBTRACT) {
console.log('DEPENDENCY MOVE')
move();
}
}
}, [dependency, question, active.activeQuestionUUID]);
console.log('Log from component B', active.activeQuestionUUID)
const goToNextQuestionWithTransition = (
where: string,
shouldPerformValidation?: boolean
) => {
// props.goToQuestion(where, shouldPerformValidation);
setInState(false);
setTimeout(() => {
props.goToQuestion(where, shouldPerformValidation);
}, 200);
};
/**
* Questions that only accept one answer will auto submit
* Questions that have more than one answer will display
* complete button after one answer is passed.
*/
const doAutoComplete = () => {
if (answerArr?.length) {
if (question.maxSelect === 1) {
goToNextQuestionWithTransition("next");
}
if (question.maxSelect > 1) {
setShowSubmitButton(true);
}
}
};
useDidUpdateEffect(() => {
doAutoComplete();
}, [answerArr]);
return (
<Grid container justify="center">
<Grid item xs={11} md={8} lg={5}>
<div className={clsx(classes.question, !inState && classes.questionOut)}>
<QuestionBody
question={question}
notifications={notifications}
module={module}
answerArr={answerArr}
/>
</div>
{showSubmitButton &&
active.activeQuestionUUID === question.uuid ? (
<Button
variant="contained"
color="secondary"
onClick={() => goToNextQuestionWithTransition("next")}
>
Ok!
</Button>
) : null}
</Grid>
</Grid>
);
};
const mapStateToProps = (state: AppState) => ({
surveyNotifications: state.surveyNotifications,
active: state.active,
answerList: state.answerList,
activeDependencies: state.activeDependencies,
});
const mapDispatchToProps = (dispatch: Dispatch) =>
bindActionCreators(
{
setActiveQuestion,
},
dispatch
);
export default connect(mapStateToProps, mapDispatchToProps)(Question);
Can you post a copy of the mapStateToProps of both component B and component A? If you are using reselect (or similar libraries), can you also post the selectors definitions?
Where are you putting the setTimeout() call?
If you are sure that there are no side effects within the mapStateToProps then it seems that you are mutating the activeQuestion property somewhere before or after the component B re-renders, assigning the old value. (Maybe you have to search for some assignement in conditions).
Also note that you can not always trust the console log, as it's value can be evaluated at later time the you call it.
I've been having trouble using React's useContext hook. I'm trying to update a state I got from my context, but I can't figure out how. I manage to change the object's property value I wanted to but I end up adding another object everytime I run this function. This is some of my code:
A method inside my "CartItem" component.
const addToQuantity = () => {
cartValue.forEach((item) => {
let boolean = Object.values(item).includes(props.name);
console.log(boolean);
if (boolean) {
setCartValue((currentState) => [...currentState, item.quantity++])
} else {
return null;
}
});
};
The "Cart Component" which renders the "CartItem"
const { cart, catalogue } = useContext(ShoppingContext);
const [catalogueValue] = catalogue;
const [cartValue, setCartValue] = cart;
const quantiFyCartItems = () => {
let arr = catalogueValue.map((item) => item.name);
let resultArr = [];
arr.forEach((item) => {
resultArr.push(
cartValue.filter((element) => item === element.name).length
);
});
return resultArr;
};
return (
<div>
{cartValue.map((item, idx) => (
<div key={idx}>
<CartItem
name={item.name}
price={item.price}
quantity={item.quantity}
id={item.id}
/>
<button onClick={quantiFyCartItems}>test</button>
</div>
))}
</div>
);
};
So how do I preserve the previous objects from my cartValue array and still modify a single property value inside an object in such an array?
edit: Here's the ShoppingContext component!
import React, { useState, createContext, useEffect } from "react";
import axios from "axios";
export const ShoppingContext = createContext();
const PRODUCTS_ENDPOINT =
"https://shielded-wildwood-82973.herokuapp.com/products.json";
const VOUCHER_ENDPOINT =
"https://shielded-wildwood-82973.herokuapp.com/vouchers.json";
export const ShoppingProvider = (props) => {
const [catalogue, setCatalogue] = useState([]);
const [cart, setCart] = useState([]);
const [vouchers, setVouchers] = useState([]);
useEffect(() => {
getCatalogueFromApi();
getVoucherFromApi();
}, []);
const getCatalogueFromApi = () => {
axios
.get(PRODUCTS_ENDPOINT)
.then((response) => setCatalogue(response.data.products))
.catch((error) => console.log(error));
};
const getVoucherFromApi = () => {
axios
.get(VOUCHER_ENDPOINT)
.then((response) => setVouchers(response.data.vouchers))
.catch((error) => console.log(error));
};
return (
<ShoppingContext.Provider
value={{
catalogue: [catalogue, setCatalogue],
cart: [cart, setCart],
vouchers: [vouchers, setVouchers],
}}
>
{props.children}
</ShoppingContext.Provider>
);
};
edit2: Thanks to Diesel's suggestion on using map, I came up with this code which is doing the trick!
const newCartValue = cartValue.map((item) => {
const boolean = Object.values(item).includes(props.name);
if (boolean && item.quantity < item.available) {
item.quantity++;
}
return item;
});
removeFromStock();
setCartValue(() => [...newCartValue]);
};```
I'm assuming that you have access to both the value and the ability to set state here:
const addToQuantity = () => {
cartValue.forEach((item) => {
let boolean = Object.values(item).includes(props.name);
console.log(boolean);
if (boolean) {
setCartValue((currentState) => [...currentState, item.quantity++])
} else {
return null;
}
});
};
Now... if you do [...currentState, item.quantity++] you will always add a new item. You're not changing anything. You're also running setCartValue on each item, which isn't necessary. I'm not sure how many can change, but it looks like you want to change values. This is what map is great for.
const addToQuantity = () => {
setCartValue((previousCartValue) => {
const newCartValue = previousCartValue.map((item) => {
const boolean = Object.values(item).includes(props.name);
console.log(boolean);
if (boolean) {
return item.quantity++;
} else {
return null;
}
});
return newCartValue;
});
};
You take all your values, do the modification you want, then you can set that as the new state. Plus it makes a new array, which is nice, as it doesn't mutate your data.
Also, if you know only one item will ever match your criteria, consider the .findIndex method as it short circuits when it finds something (it will stop there), then modify that index.
I'm trying to build a simple todo-app using react-redux. Problem is when I'm trying to update data, it would not update in view. My code is given below:
actions
export const listTodo = () => { type: actionTypes.LIST_TODO }
export const updateTodo = ( payload, index ) => { type: actionTypes.UPDATE_TODO, payload, index }
Reducers
const INITIAL_STATE = {
all: [{
name: 'init',
status: false,
lastUpdate: new Date().toISOString()
}]
}
const listTodo = ( state, action ) => {...state, all: state.all }
const updateTodo = ( state, action ) => {
const listTodo = {...state, all: state.all }; // init data
// find data
let todo = listTodo.all.filter( (todo, index) => index === action.index );
// update data
todo.name = action.payload.name;
todo.status = action.payload.status;
todo.lastUpdate = new Date().toISOString();
listTodo.all[ action.index ] = todo;
// return data
return {
...state,
all: listTodo.all
}
}
export default ( state = INITIAL_STATE, action) => {
switch( action.type ) {
case LIST_TODO:
return listTodo( state, action );
case UPDATE_TODO:
return updateTodo( state, action );
default:
return state;
}
}
In below code (Components/list.js), I just fetch all todo-list, and then print all list using ShowList.
Components/list.js
import ShowList from '../container/showList';
class TodoList extends Component {
renderTodoList() {
return this.props.all.map( (todo, index) => {
return (
<ShowList key={index} index={index} todo={todo} />
);
});
}
render() {
return <ul> { this.renderTodoList() } </ul>
}
}
const mapStateToProps = ( state ) => { all: state.todo.all };
export default connect( mapStateToProps ) ( TodoList );
In below code (container/showList.js), todo list is shown using <li /> and also have a checkbox, when user click on checkbox, handleCheckbox will trigger, and will update todo-list. I believe data is updated correctly, but it is not updated on html. In browser, todo-list remain same as before.
container/showList.js
class ShowList extends Component {
handleCheckbox = ( ) => {
const { todo, index } = this.props;
todo.status = !todo.status;
todo.lastUpdate = new Date().toISOString();
this.props.onUpdateTodo( todo, index );
}
render() {
const { todo, index } = this.props;
return (
<li> <input type="checkbox" onChange={ this.handleCheckbox } checked={todo.status} /> {todo.name} # {todo.status.toString()} # { todo.lastUpdate } </li>
)
}
}
const mapDispatchToProps = ( dispatch ) => onUpdateTodo: ( todo, index ) => dispatch( actions.updateTodo( todo, index ) )
export default connect(null, mapDispatchToProps) (ShowList);
How can I solve this problem? Thanks in Advance.
Your problem was in your reducers file. Whenever you executed updateToDo() you were not actually updating the the existing todos, you would just add a new property to your state with the new changes. This created layers and layers of properties without actually updating the first-layer. And since your components were only connected to the first-layer, it would never get the updated state.
I've updated a codesandbox for your reference: https://codesandbox.io/s/lively-flower-mwh79
You can update your reducers to something like this and then your code works completely fine:
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case "LIST_TODO":
return listTodo(state, action);
case "UPDATE_TODO":
return {
...state,
all: state.all.map((todo, index) => {
if (index == action.index) {
return {
...todo,
status: todo.status,
lastUpdate: new Date().toISOString()
};
} else {
return todo;
}
})
};
default:
return state;
}
};
Your problem is in this line,
export const updateTodo = ( payload, index ) => { type: actionTypes.UPDATE_TODO, payload, index }
Redux Action will only take 2 parameters, type and payload respectively.
Here you are passing 3 parameters which is wrong. Remove your index parameter, then your action becomes like this,
export const updateTodo = ( payload, index ) => { type: actionTypes.UPDATE_TODO, payload } //payload = your updated todo list
Pass only update todo list to your action,
this.props.onUpdateTodo( todo );
Finally in your reducer only do this,
return Object.assign(state,action.todo) // This will merge your old state with updated todo list and eventually you will get a updated list.
See more obout Object.assign here