Related
I'm using the computed() method to add some data to a ref() array of objects.
Using this computed array of objects works for reading data, for example using v-for, but it is ignored (nothing happens) when I'm trying to update the data, the example below shows the working vs not working code.
In the useCart composable (see code below), I created a computedCartItems which maps the cart and adds the totalPrice for each item. Now in my index.vue file, I try to increase the amount for a cartItem, this works if I loop over the cart using <div v-for="cartItem in cart"> but it is ignored when using the computed object <div v-for="cartItem in computedCartItems">
useCart.js
const useCart = () => {
const cart = ref([])
const computedCartItems = computed(() => {
return cart.value.map(cartItem => {
return {
...cartItem,
totalPrice: cartItem.amount * cartItem.price
}
})
})
return {
cart,
computedCartItems,
}
}
export default useCart
index.vue (not working, using computed 'computedCartItems' object)
<div v-for="cartItem in computedCartItems">
<div>
<div>{{ cartItem.name }}</div>
<button #click="onIncrement(cartItem)">+</button>
</div>
</div>
<script setup>
const { cart, computedCartItems } = useCart()
const onIncrement = ({ id }) => {
const shoppingCartItemIndex = computedCartItems.value.findIndex(item => item.id === id)
computedCartItems.value[shoppingCartItemIndex].amount++
}
</script>
index.vue (working, using original 'cart' object)
<div v-for="cartItem in cart">
<div>
<div>{{ cartItem.name }}</div>
<button #click="onIncrement(cartItem)">+</button>
</div>
</div>
<script setup>
const { cart, computedCartItems } = useCart()
const onIncrement = ({ id }) => {
const shoppingCartItemIndex = cart.value.findIndex(item => item.id === id)
cart.value[shoppingCartItemIndex].amount++
}
</script>
TLDR; you're updating values on a copy of your original object. They are not linked so the original object doesn't receive the updated value.
Detailed anwser
Computeds are readonly. They are derivated data and should not be updated.
Because this is javascript, you can update the object attributes by reference, but you really shouldn't, this is a bad practise leading to unclear side effects.
See the typescript type of computed:
export declare interface ComputedRef<T = any> extends WritableComputedRef<T> {
readonly value: T;
[ComputedRefSymbol]: true;
}
So myComputed.value is readonly and cannot be assigned another value. You can still do myComputed.value.myProperty = 'foo' but, as mentioned, this is a bad practise.
More information on this on the official documentation
A possible solution
Create the totalPrice composable for each item, not for the entire cart, and assign the computed inside your item object.
const useItem = (reactiveItem) => {
const totalPrice = computed(() => reactiveItem.amount * reactiveItem.price)
// Assign a new property in your item, which is the derived totalPrice
reactiveItem.totalPrice = totalPrice
return reactiveItem
}
const useCart = () => {
const cart = ref([])
// Export a custom function to include the item and make it reactive + use composable (saves the final client from doing it)
const addItem = (item) => {
cart.value.push(useItem(reactive(item)))
}
return { cart, addItem }
}
const { cart, addItem } = useCart()
function createItem() {
addItem({ amount: 5, price: 10 })
}
Check this online playground with a working example.
I'm sure there are other ways of doing it, this is only one. You could use watch to react for your cart changes for example.
The Core Issue
A computed ref is derived data: it represents your data in some way; you do not update it directly, you update its sources.
There is a section about this in the docs which explains the issue quite succinctly:
Avoid mutating computed value
The returned value from a computed property is derived state. Think of it as a temporary snapshot - every time the source state changes, a new snapshot is created. It does not make sense to mutate a snapshot, so a computed return value should be treated as read-only and never be mutated - instead, update the source state it depends on to trigger new computations.
In your non-working example, you are not trying to update the actual computed ref (which is not even possible; see the doc references at the end of the answer); you are updating properties of the ref's value, which you can -- but shouldn't -- do. However, aside from all the other problems, the computed will not update, as the total price is based on the original item in cart, not the one in the computed, meaning an update is never triggered (as cart is not changed).
If you instead modify the source ref (cart), the computed ref will update and the example will work:
<!-- Use `computedCartItems` here -->
<div v-for="cartItem in computedCartItems">
<div>
<div>{{ cartItem.name }}</div>
<button #click="onIncrement(cartItem)">+</button>
</div>
</div>
<script setup>
const { cart, computedCartItems } = useCart()
const onIncrement = ({ id }) => {
// Use `cart` here.
const shoppingCartItemIndex = cart.value.findIndex(item => item.id === id)
cart.value[shoppingCartItemIndex].amount++
}
</script>
A (Possibly) Better Way
While this works, it is quite possibly not the ideal way to go about solving your particular case. Every time an item is updated, the whole computed array and every item in it is recreated, which is very inefficient.
Instead, you can make the useCart composable only return the single cart ref along with some methods to manipulate the cart. You could do something like this:
import { ref, reactive, computed, readonly } from 'vue'
const useCart = () => {
const cart = ref([])
/**
Add a new item to the cart.
Makes the item reactive (so that there is a reactive source for computed properties),
adds the `totalPrice` computed property, and appends it to the cart array.
*/
const addItem = (item) => {
const reactiveItem = reactive(item)
reactiveItem.totalPrice = computed(() => reactiveItem.amount * reactiveItem.price)
cart.value.push(reactiveItem)
}
/**
Increase the amount of an item.
You could add all kinds of methods like these.
*/
const increaseAmount = (id) => {
const index = cart.value.findIndex((item) => item.id === id)
cart.value[index].amount += 1
}
return {
cart: readonly(cart), // So that the cart cannot be modified directly by the consumer.
addItem,
increaseAmount
}
}
const { cart, addItem, increaseAmount } = useCart()
addItem({ id: "1", amount: 5, price: 10 })
console.log(cart.value[0].totalPrice) // 50
Now the handling of the cart is done by the useCart composable, making things easier for the consumer by abstracting away internals. In addition to the gains mentioned above, this also means that the composable remains in control of its data, as the cart ref cannot just be modified. "Separation of concerns", etc.
Documentation References and Such
Vue Docs
Computed Properties - Vue.js Docs
The whole point of computed refs is that they update automatically based on their sources. You do not modify them directly, you modify their sources.
A computed property automatically tracks its reactive dependencies. Vue is aware that the computation of publishedBooksMessage depends on author.books, so it will update any bindings that depend on publishedBooksMessage when author.books changes.
You cannot assign a value to a regular computed ref.
Computed properties are by default getter-only. If you attempt to assign a new value to a computed property, you will receive a runtime warning. In the rare cases where you need a "writable" computed property, you can create one by providing both a getter and a setter.
I highly recommend reading the "Reactivity Fundamentals" section of the Vue Guide. See especially "Ref Unwrapping in Reactive Objects" for some insight on how the nesting of the computed ref inside the reactive works.
I also suggest going through the entire "Reactivity in Depth" page when you're ready. It gives you a grip on how the reactivity system actually works.
Other Links
VueUse is a great resource, both for many handy composables and for learning.
I've been playing around with the new hook system in React 16.7-alpha and get stuck in an infinite loop in useEffect when the state I'm handling is an object or array.
First, I use useState and initiate it with an empty object like this:
const [obj, setObj] = useState({});
Then, in useEffect, I use setObj to set it to an empty object again. As a second argument I'm passing [obj], hoping that it wont update if the content of the object hasn't changed. But it keeps updating. I guess because no matter the content, these are always different objects making React thinking it keep changing?
useEffect(() => {
setIngredients({});
}, [ingredients]);
The same is true with arrays, but as a primitive it wont get stuck in a loop, as expected.
Using these new hooks, how should I handle objects and array when checking weather the content has changed or not?
Passing an empty array as the second argument to useEffect makes it only run on mount and unmount, thus stopping any infinite loops.
useEffect(() => {
setIngredients({});
}, []);
This was clarified to me in the blog post on React hooks at https://www.robinwieruch.de/react-hooks/
Had the same problem. I don't know why they not mention this in docs. Just want to add a little to Tobias Haugen answer.
To run in every component/parent rerender you need to use:
useEffect(() => {
// don't know where it can be used :/
})
To run anything only one time after component mount(will be rendered once) you need to use:
useEffect(() => {
// do anything only one time if you pass empty array []
// keep in mind, that component will be rendered one time (with default values) before we get here
}, [] )
To run anything one time on component mount and on data/data2 change:
const [data, setData] = useState(false)
const [data2, setData2] = useState('default value for first render')
useEffect(() => {
// if you pass some variable, than component will rerender after component mount one time and second time if this(in my case data or data2) is changed
// if your data is object and you want to trigger this when property of object changed, clone object like this let clone = JSON.parse(JSON.stringify(data)), change it clone.prop = 2 and setData(clone).
// if you do like this 'data.prop=2' without cloning useEffect will not be triggered, because link to data object in momory doesn't changed, even if object changed (as i understand this)
}, [data, data2] )
How i use it most of the time:
export default function Book({id}) {
const [book, bookSet] = useState(false)
const loadBookFromServer = useCallback(async () => {
let response = await fetch('api/book/' + id)
response = await response.json()
bookSet(response)
}, [id]) // every time id changed, new book will be loaded
useEffect(() => {
loadBookFromServer()
}, [loadBookFromServer]) // useEffect will run once and when id changes
if (!book) return false //first render, when useEffect did't triggered yet we will return false
return <div>{JSON.stringify(book)}</div>
}
I ran into the same problem too once and I fixed it by making sure I pass primitive values in the second argument [].
If you pass an object, React will store only the reference to the object and run the effect when the reference changes, which is usually every singe time (I don't now how though).
The solution is to pass the values in the object. You can try,
const obj = { keyA: 'a', keyB: 'b' }
useEffect(() => {
// do something
}, [Object.values(obj)]);
or
const obj = { keyA: 'a', keyB: 'b' }
useEffect(() => {
// do something
}, [obj.keyA, obj.keyB]);
If you are building a custom hook, you can sometimes cause an infinite loop with default as follows
function useMyBadHook(values = {}) {
useEffect(()=> {
/* This runs every render, if values is undefined */
},
[values]
)
}
The fix is to use the same object instead of creating a new one on every function call:
const defaultValues = {};
function useMyBadHook(values = defaultValues) {
useEffect(()=> {
/* This runs on first call and when values change */
},
[values]
)
}
If you are encountering this in your component code the loop may get fixed if you use defaultProps instead of ES6 default values
function MyComponent({values}) {
useEffect(()=> {
/* do stuff*/
},[values]
)
return null; /* stuff */
}
MyComponent.defaultProps = {
values = {}
}
Your infinite loop is due to circularity
useEffect(() => {
setIngredients({});
}, [ingredients]);
setIngredients({}); will change the value of ingredients(will return a new reference each time), which will run setIngredients({}). To solve this you can use either approach:
Pass a different second argument to useEffect
const timeToChangeIngrediants = .....
useEffect(() => {
setIngredients({});
}, [timeToChangeIngrediants ]);
setIngrediants will run when timeToChangeIngrediants has changed.
I'm not sure what use case justifies change ingrediants once it has been changed. But if it is the case, you pass Object.values(ingrediants) as a second argument to useEffect.
useEffect(() => {
setIngredients({});
}, Object.values(ingrediants));
As said in the documentation (https://reactjs.org/docs/hooks-effect.html), the useEffect hook is meant to be used when you want some code to be executed after every render. From the docs:
Does useEffect run after every render? Yes!
If you want to customize this, you can follow the instructions that appear later in the same page (https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects). Basically, the useEffect method accepts a second argument, that React will examine to determine if the effect has to be triggered again or not.
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes
You can pass any object as the second argument. If this object remains unchanged, your effect will only be triggered after the first mount. If the object changes, the effect will be triggered again.
I'm not sure if this will work for you but you could try adding .length like this:
useEffect(() => {
// fetch from server and set as obj
}, [obj.length]);
In my case (I was fetching an array!) it fetched data on mount, then again only on change and it didn't go into a loop.
If you include empty array at the end of useEffect:
useEffect(()=>{
setText(text);
},[])
It would run once.
If you include also parameter on array:
useEffect(()=>{
setText(text);
},[text])
It would run whenever text parameter change.
I often run into an infinite re-render when having a complex object as state and updating it from useRef:
const [ingredients, setIngredients] = useState({});
useEffect(() => {
setIngredients({
...ingredients,
newIngedient: { ... }
});
}, [ingredients]);
In this case eslint(react-hooks/exhaustive-deps) forces me (correctly) to add ingredients to the dependency array. However, this results in an infinite re-render. Unlike what some say in this thread, this is correct, and you can't get away with putting ingredients.someKey or ingredients.length into the dependency array.
The solution is that setters provide the old value that you can refer to. You should use this, rather than referring to ingredients directly:
const [ingredients, setIngredients] = useState({});
useEffect(() => {
setIngredients(oldIngedients => {
return {
...oldIngedients,
newIngedient: { ... }
}
});
}, []);
If you use this optimization, make sure the array includes all values from the component scope (such as props and state) that change over time and that are used by the effect.
I believe they are trying to express the possibility that one could be using stale data, and to be aware of this. It doesn't matter the type of values we send in the array for the second argument as long as we know that if any of those values change it will execute the effect. If we are using ingredients as part of the computation within the effect, we should include it in the array.
const [ingredients, setIngredients] = useState({});
// This will be an infinite loop, because by shallow comparison ingredients !== {}
useEffect(() => {
setIngredients({});
}, [ingredients]);
// If we need to update ingredients then we need to manually confirm
// that it is actually different by deep comparison.
useEffect(() => {
if (is(<similar_object>, ingredients) {
return;
}
setIngredients(<similar_object>);
}, [ingredients]);
The main problem is that useEffect compares the incoming value with the current value shallowly. This means that these two values compared using '===' comparison which only checks for object references and although array and object values are the same it treats them to be two different objects. I recommend you to check out my article about useEffect as a lifecycle methods.
The best way is to compare previous value with current value by using usePrevious() and _.isEqual() from Lodash.
Import isEqual and useRef. Compare your previous value with current value inside the useEffect(). If they are same do nothing else update. usePrevious(value) is a custom hook which create a ref with useRef().
Below is snippet of my code. I was facing problem of infinite loop with updating data using firebase hook
import React, { useState, useEffect, useRef } from 'react'
import 'firebase/database'
import { Redirect } from 'react-router-dom'
import { isEqual } from 'lodash'
import {
useUserStatistics
} from '../../hooks/firebase-hooks'
export function TMDPage({ match, history, location }) {
const usePrevious = value => {
const ref = useRef()
useEffect(() => {
ref.current = value
})
return ref.current
}
const userId = match.params ? match.params.id : ''
const teamId = location.state ? location.state.teamId : ''
const [userStatistics] = useUserStatistics(userId, teamId)
const previousUserStatistics = usePrevious(userStatistics)
useEffect(() => {
if (
!isEqual(userStatistics, previousUserStatistics)
) {
doSomething()
}
})
In case you DO need to compare the object and when it is updated here is a deepCompare hook for comparison. The accepted answer surely does not address that. Having an [] array is suitable if you need the effect to run only once when mounted.
Also, other voted answers only address a check for primitive types by doing obj.value or something similar to first get to the level where it is not nested. This may not be the best case for deeply nested objects.
So here is one that will work in all cases.
import { DependencyList } from "react";
const useDeepCompare = (
value: DependencyList | undefined
): DependencyList | undefined => {
const ref = useRef<DependencyList | undefined>();
if (!isEqual(ref.current, value)) {
ref.current = value;
}
return ref.current;
};
You can use the same in useEffect hook
React.useEffect(() => {
setState(state);
}, useDeepCompare([state]));
You could also destructure the object in the dependency array, meaning the state would only update when certain parts of the object updated.
For the sake of this example, let's say the ingredients contained carrots, we could pass that to the dependency, and only if carrots changed, would the state update.
You could then take this further and only update the number of carrots at certain points, thus controlling when the state would update and avoiding an infinite loop.
useEffect(() => {
setIngredients({});
}, [ingredients.carrots]);
An example of when something like this could be used is when a user logs into a website. When they log in, we could destructure the user object to extract their cookie and permission role, and update the state of the app accordingly.
my Case was special on encountering an infinite loop, the senario was like this:
I had an Object, lets say objX that comes from props and i was destructuring it in props like:
const { something: { somePropery } } = ObjX
and i used the somePropery as a dependency to my useEffect like:
useEffect(() => {
// ...
}, [somePropery])
and it caused me an infinite loop, i tried to handle this by passing the whole something as a dependency and it worked properly.
Another worked solution that I used for arrays state is:
useEffect(() => {
setIngredients(ingredients.length ? ingredients : null);
}, [ingredients]);
Consider the code :
import React, { useState, useEffect } from 'react';
........ More stuff
const ProductContext = React.createContext();
const ProductConsumer = ProductContext.Consumer;
const ProductProvider = ({ children }) => {
const [state, setState] = useState({
sideBarOpen: false,
cartOpen: true,
cartItems: 10,
links: linkData,
socialIcons: socialData,
cart: [],
cartSubTotal: 0,
cartTax: 0,
cartTotal: 0,
.......
loading: true,
cartCounter: 0,
});
const getTotals = () => {
// .. Do some calculations ....
return {
cartItems,
subTotal,
tax,
total,
};
};
const addTotals = () => {
const totals = getTotals();
setState({
...state,
cartItems: totals.cartItems,
cartSubTotal: totals.subTotal,
cartTax: totals.tax,
cartTotal: totals.total,
});
};
/**
* Use Effect only when cart has been changed
*/
useEffect(() => {
if (state.cartCounter > 0) {
addTotals();
syncStorage();
openCart();
}
}, [state.cartCounter]);
..... More code
return (
<ProductContext.Provider
value={{
...state,
............... More stuff
}}
>
{children}
</ProductContext.Provider>
);
};
export { ProductProvider, ProductConsumer };
This is a Context of a Shopping cart ,whenever the user add a new item to the cart
this piece of code runs :
useEffect(() => {
if (state.cartCounter > 0) {
addTotals();
syncStorage();
openCart();
}
}, [state.cartCounter]);
And updates the state , however the setState function doesn't update state
when running :
setState({
...state,
cartItems: totals.cartItems,
cartSubTotal: totals.subTotal,
cartTax: totals.tax,
cartTotal: totals.total,
});
Inside addTotals , even though this function is being called automatically when UseEffect detects that state.cartCounter has been changed.
Why aren't the changes being reflected in the state variable ?
Without a stripped down working example, I can only guess at the problems...
Potential Problem 1
You're calling a callback function in useEffect which should be added to it's [dependencies] for memoization.
const dep2 = React.useCallback(() => {}, []);
useEffect(() => {
if(dep1 > 0) {
dep2();
}
}, [dep1, dep2]);
Since dep2 is a callback function, if it's not wrapped in a React.useCallback, then it could potentially cause an infinite re-render if it's changed.
Potential Problem 2
You're mutating the state object or one of its properties. Since I'm not seeing the full code, this is only an assumption. But Array methods like: splice, push, unshift, shift, pop, sort to name a few cause mutations to the original Array. In addition, objects can be mutated by using delete prop or obj.name = "example" or obj["total"] = 2. Again, without the full code, it's just a guess.
Potential Problem 3
You're attempting to spread stale state when it's executed. When using multiple setState calls to update an object, there's no guarantee that the state is going to be up-to-date when it's executed. Best practice is to pass setState a function which accepts the current state as an argument and returns an updated state object:
setState(prevState => ({
...prevState,
prop1: prevState.prop1 + 1
}));
This ensures the state is always up-to-date when it's being batch executed. For example, if the first setState updates cartTotal: 11, then prevState.cartTotal is guaranteed to be 11 when the next setState is executed.
Potential Problem 4
If state.cartCounter is ever updated within this component, then this will cause an infinite re-render loop because the useEffect listens and fires every time it changes. This may or may not be a problem within your project, but it's something to be aware of. A workaround is to trigger a boolean to prevent addTotals from executing more than once. Since the prop name "cartCounter" is a number and is rather ambiguous to its overall functionality, then it may not be the best way to update the cart totals synchronously.
React.useEffect(() => {
if (state.cartCounter > 0 && state.updateCart) {
addTotals();
...etc
}
}, [state.updateCart, state.cartCounter, addTotals]);
Working demo (click the Add to Cart button to update cart state):
If neither of the problems mentioned above solves your problem, then I'd recommend creating a mwe. Otherwise, it's a guessing game.
I am learning react hooks. I am having mock data js call "MockFireBase.js" as below:
const userIngredientsList = [];
export const Get = () => {
return userIngredientsList;
}
export const Post = (ingredient) => {
ingredient.id = userIngredientsList.length + 1;
userIngredientsList.push(ingredient);
return ingredient;
}
Then my react hooks component "Ingredients.js" will call this mock utilities as following details:
const Ingredients = () => {
const [userIngredients, setUserIngredients] = useState([]);
// only load one time
useEffect(() => { setUserIngredients(Get()); }, []);
const addIngredienHandler = ingredient => {
let responsData = Post(ingredient);
setUserIngredients(preIngredients => {
return [...preIngredients, responsData]
});
}
return (
<div className="App">
<IngredientForm onAddIngredient={addIngredienHandler} />
<section>
<IngredientList ingredients={userIngredients} />
</section>
</div>
);
)
}
When I added first ingredient, it added two (of course I get same key issue in console.log). Then I added second ingredient is fine.
If I remove the useEffect code as below, it will work good.
// only load one time
useEffect(() => { setUserIngredients(loadedIngredients); }, []);
I am wondering what I did anything wrong above, if I use useEffect
The problem is not in useEffect. It's about mutating a global userIngredientsList array.
from useEffect you set initial component state to be userIngredientsList.
Then inside addIngredienHandler you call Post(). This function does two things:
2a. pushes the new ingredient to the global userIngredientsList array`. Since it's the same instance as you saved in your state in step 1, your state now contains this ingredient already.
2a. Returns this ingredient
Then, addIngredienHandler adds this ingredient to the state again - so you end up having it in the state twice.
Fix 1
Remove userIngredientsList.push(ingredient); line from your Post function.
Fix 2
Or, if you need this global list of ingredients for further usage, you should make sure you don't store it in your component state directly, and instead create a shallow copy in your state:
useEffect(() => { setUserIngredients([...Get()]); }, []);
Im wracking my brain trying to understand the component structure here that I should employ. I feel like this is something that I absolutely want to get correctly because going forward it's important to understand how a ReactJS application should look and how to correctly separate the concerns. I know it is an opinionated front but how I am currently doing it is not correct, and I was looking for some insight.
Data Model: This is a large array of Recipes, each contains another array of ingredients. I want to allow the user to "tick off" (the ingredient is removed from the array) the ingredients as they buy them/acquire them.
[
...
{
"title": "Recipe 1"
"ingredients": [
{ "title": "Flour", "measurement": "300g" },
{ "title": "Sesame Seeds", "measurement": "1 Tblsp" }
]
},
...
]
Current Psuedo Component Tree:
// RecipeList is my "HOC" (higher-order component) and contains all the functions/data required for the rest of the tree, and simply passes them down as props. This is `connect()`ed to the store here.
<RecipeList>
// Map on the highest level array to display a <Recipe> component for each
<Recipe data={recipe[mappedIndex]}>
// Map on each ingredient to create a list item for each
<RecipeIngredient data={recipe[mappedIndex].ingredient[mappedIndex]>
<IngredientCheckBox onChange={ //remove this ingredient from the array } />
</RecipeIngredient>
</Recipe>
</RecipeList>
All is well with the above, the data is displayed exactly how I would expect it to.
However, and this is the main issue, when it comes to onChange I call an action, COMPLETE_INGREDIENT which basically removes it from the ingredients array (I see it in action, using redux-logger the next-state does not contain it).
Unfortunately, my components don't rerender. It is no longer in the array but is still displayed on screen. I understand this may one of the following reasons:
connect() only shallow compares the states so doesn't trigger a rerender because it is a value in an array, inside of a property of an object in an array.
My connect() is too far from the action, and should be reconnect()ed at a deeper component level, say the <Recipe> component and only attach it to a part of the store that it cares about (could even be the <RecipeIngredient>).
My reducer is not modifying the state in an immutable way. This is the one I have spent most time on, however even using slice() and the sorts, I still can't get it to re-render
Edit: My reducer for action COMPLETE_INGREDIENT. I understand this may be the issue, as it is directly mutating the state. What would be the correct way for such a deep change to the state?
case COMPLETE_INGREDIENT:
// state is from a level above the recipe's, that contains a timestamp etc
state.recipes[action.payload.recipeIndex].ingredients.splice(action.payload.ingredientIndex, 1)
return Object.assign({
...state
})
Edit: My reducer for action COMPLETE_INGREDIENT. I understand this may
be the issue, as it is directly mutating the state. What would be the
correct way for such a deep change to the state?
Yep, you are mutating state with that Object.assign. As a first argument it should have new Object to copy values to and return:
return Object.assign({}, {
...state
})
Based on your code I've created updating function I would probably create:
case COMPLETE_INGREDIENT: {
const { recipeIndex, ingregientIndex } = action.payload;
const recipesListCopy = [...state.recipes];
const recipeCopy = {
...recipesListCopy[recipeIndex],
ingredients: recipesListCopy[recipeIndex].ingredients.filter(
(e, index) => index !== ingredientIndex
)
};
recipesListCopy[recipeIndex] = recipeCopy;
return {
...state,
recipes: recipesListCopy
};
}
Edit:
based on your comment - "remove the recipe from the top level recipe array if the ingredients array is now empty"
case COMPLETE_INGREDIENT: {
const { recipeIndex, ingregientIndex } = action.payload;
const recipesListCopy = [...state.recipes];
const updatedIngredientsList = recipesListCopy[recipeIndex].ingredients.filter(
(e, index) => index !== ingredientIndex
);
if(updatedIngredientsList.length > 0) {
// update ingredients
const recipeCopy = {
...recipesListCopy[recipeIndex],
ingredients: updatedIngredientsList
};
recipesListCopy[recipeIndex] = recipeCopy;
} else {
// remove recipe because no igridients
recipesListCopy.splice(recipeIndex, 1);
}
return {
...state,
recipes: recipesListCopy
};
}
Problem here is in mutating existing state
case COMPLETE_INGREDIENT:
// state is from a level above the recipe's, that contains a timestamp etc
state.recipes[action.payload.recipeIndex].ingredients.splice(action.payload.ingredientIndex, 1)
return Object.assign({
...state
})
Method splice changes existing array, but you need to create new array. Here is example
const obj = { a: { b: 5 } };
const copyObj = { ...obj };
copyObj.a.b = 1;
console.log(obj.a.b); // 1
console.log(copyObj.a.b); // 1
You have copied state object but ingredients array stays the same. So you need to copy array.
case COMPLETE_INGREDIENT:
return {
...state,
recipes: state.recipes.map(
(item, index) =>
index === action.payload.recipeIndex
? { ...item, ingredients: item.ingredients.filter((item, index) => index !== action.payload.ingredientIndex) }
: item
),
};