I am new at ReactJs and have a question about this two method:
1:
handleLike = movie => {
const movies = this.state.movies.map(m => {
if (m._id === movie._id) m.liked = !m.liked;
return m;
});
this.setState({ movies });
};
2:
handleLike = movie => {
const movies = [...this.state.movies];
const index = movies.indexOf(movie);
movies[index] = { ...movies[index] };
movies[index].liked = !movies[index].liked;
this.setState({ movies });
};
Q1: This two methods just toggle liked and work properly but i want to know there is any advantages or not?
Q2: What is purpose of this line in second method:
movies[index] = { ...movies[index] };
Don't use #1, at least not the way it is written. You are mutating the old state, which can easily cause bugs in react which assumes that state is immutable. You do create a new array, which is good, but you're not creating new elements inside the array. If you're changing one of the objects in the array, you need to copy that object before modifying it.
The better way to do #1 would be:
handleLike = movie => {
const movies = this.state.movies.map(m => {
if (m._id === movie._id) {
const copy = { ...m };
copy.liked = !m.liked;
return copy;
}
return m;
});
this.setState({ movies });
};
And that kind of gets to your question about #2 as well:
Q2: What is purpose of this line in second method:
movies[index] = { ...movies[index] };
The purpose of that is to make a copy of the movie. This lets you make a change to the copy, without modifying the old state.
Q1: This two methods just toggle liked and work properly but i want to know there is any advantages or not?
If you fix the mutation issue in #1, then it's pretty much a matter of preference.
Related
I want to understand why the code can not work by copying the array from the state. checkboxes is an array of Booleans.
I was of the opinion that if I assign checkboxes from state directly to tempCheckboxes it will be an alias and modifying an alias will indirectly impact state variable. But that is not the case. And I want to know why? Thanks for your help.
checkRow = (index, checked) => {
let tempCheckboxes = this.state.checkboxes.slice();
let all = true;
tempCheckboxes[index] = checked;
all = tempCheckboxes.every((isChecked) => this.isTrue(isChecked));
this.setState((prevState) => ({ ...prevState, checkboxes: tempCheckboxes, checkAll: all }));
};
Above code fails to work but following code works
checkRow = (index, checked) => {
let tempCheckboxes = this.state.checkboxes
let all = true;
tempCheckboxes[index] = checked;
all = tempCheckboxes.every((isChecked) => this.isTrue(isChecked));
this.setState((prevState) => ({ ...prevState, checkboxes: tempCheckboxes, checkAll: all }));
};
Edit : Reason for this question is in react we should not directly change the state with assignment but instead use setState to do so. In my opinion 2nd version of code is doing it. Earlier title for the question was not precise so changed it.
So the question is about javascript object reference.
Slice does not alter the original array, while the direct = which you are doing is creating a reference to the original object. So what you change in tempCheckboxes, will directly affect this.state.checkboxes.
In your second option tempCheckboxes will be a reference to the original this.state.checkboxes
In the first option you will refer to a new object in memory.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice
Im using NGRX store on angular project.
This is the state type:
export interface TagsMap {
[key:number]: { // lets call this key - user id.
[key: number]: number // and this key - tag id.
}
}
So for example:
{5: {1: 1, 2: 2}, 6: {3: 3}}
User 5 has tags: 1,2, and user 6 has tag 3.
I have above 200k keys in this state, and would like to make the updates as efficient as possible.
The operation is to add a tag to all the users in the state.
I tried the best practice approach like so :
const addTagToAllUsers = (state, tagIdToAdd) => {
const userIds = Object.keys(state.userTags);
return userIds.reduce((acc, contactId, index) => {
return {
...acc,
[contactId]: {
...acc[contactId],
[tagIdToAdd]: tagIdToAdd
}
};
}, state.userTags);
};
But unfortunately this makes the browser crush when there are over 200k users and around 5 tags each.
I managed to make it work with this:
const addTagToAllUsers = (state, tagIdToAdd) => {
const stateUserTagsShallowCopy = {...state.userTags};
const userIds = Object.keys(stateUserTags);
for (let i = 0; i <= userIds.length - 1; i++) {
const currUserId = userIds[i];
stateUserTagsShallowCopy[currUserId] = {
...stateUserTagsShallowCopy[currUserId],
[tagIdToAdd]: tagIdToAdd
};
}
return stateUserTagsShallowCopy;
};
And the components are updated from the store nicely without any bugs as far as I checked.
But as written here: Redux website mentions :
The key to updating nested data is that every level of nesting must be copied and updated appropriately
Therefore I wonder if my solution is bad.
Questions:
I Believe I'm still shallow coping all levels in state, am i wrong ?
Is it a bad solution? if so what bugs may it produce that I might be missing ?
Why is it required to update sub nested level state in an immutable manner, if the store selector will still fire because the parent reference indeed changed. (Since it works with shallow checks on the top level.)
What is the best efficient solution ?
Regarding question 3, here is an example of the selector code :
import {createFeatureSelector, createSelector, select} from '#ngrx/store';//ngrx version 10.0.0
//The reducer
const reducers = {
userTags: (state, action) => {
//the reducer function..
}
}
//then on app module:
StoreModule.forRoot(reducers)
//The selector :
const stateToUserTags = createSelector(
createFeatureSelector('userTags'),
(userTags) => {
//this will execute whenever userTags state is updated, as long as it passes the shallow check comparison.
//hence the question why is it required to return a new reference to every nested level object of the state...
return userTags;
}
)
//this.store is NGRX: Store<State>
const tags$: Observable<any> = this.store.pipe(select(stateToUser))
//then in component I use it something like this:
<tagsList tags=[tags$ | async]>
</tagsList>
Your solution is perfectly fine.
The rule of thumb is that you cannot mutate an object/array stored in state.
In Your example, the only thing that You are mutating is the stateUserTagsShallowCopy object (it is not stored inside state since it is a shallow copy of state.userTags).
Sidenote: It is better to use for of here since you don't need to access the index
const addTagToAllUsers = (state, tagIdToAdd) => {
const stateUserTagsShallowCopy = {...state.userTags};
const userIds = Object.keys(stateUserTags);
for (let currUserId of userIds) {
stateUserTagsShallowCopy[currUserId] = {
...stateUserTagsShallowCopy[currUserId],
[tagIdToAdd]: tagIdToAdd
};
}
return stateUserTagsShallowCopy;
};
If you decide to use immer this will look like this
import produce from "immer";
const addTagToAllUsers = (state, tagIdToAdd) => {
const updatedStateUserTags = produce(state.userTags, draft => {
for (let draftTags of Object.values(draft)) {
tags[tagIdToAdd] = tagIdToAdd
}
})
return updatedStateUserTags
});
(this comes usually with performance cost). With immer you can sacrifice performance to gain readability
ad 3.
Why is it required to update sub nested level state in an immutable manner, if the store selector will still fire because the parent reference indeed changed. (Since it works with shallow checks on the top level.)
Every store change selectors recompute to see if the dependent component should re-render.
imagine that instead of immutable update of the user tags we decided to mutate tags inside user (state.userTags is a new object reference but we mutate (reuse) old entries objects state.userTags[userId])
const addTagToAllUsers = (state, tagIdToAdd) => {
const stateUserTagsShallowCopy = {...state.userTags};
const userIds = Object.keys(stateUserTags);
for (let currUserId of userIds) {
stateUserTagsShallowCopy[currUserId][tagIdToAdd] = tagIdToAdd;
}
return stateUserTagsShallowCopy;
};
In your case, you have a selector that takes out state.userTags.
It means that every time a state update happens nrgx will compare the previous result of the selector and the current one (prevUserTags === currUserTags by reference). In our case, we change state.userTags so the component that uses this selector will be refreshed with new userTags.
But imagine other selectors that instead of all userTags will select only one user tags. In our imaginary situation, we mutate directly userTags[someUserId] so the reference remains the same each time. The negative effect here is that subscribing component will be not refreshed (will not see an update after a tag is added).
I'm a student of functional programming. I am still weaning myself off the old variable-mutation habits. But sometimes I get stuck. Ok, so here is the question--suppose we have the following closure
const bookShelf = () => {
let books = []
const listBooks = () => books
const addBook = (book) => {
books = books.concat(book)
return function removeBook() { books = books.filter( b => b !== book ) }
}
return {addBook,listBooks}
}
const { addBook, listBooks } = bookShelf()
const removeMobyDick = addBook('Moby Dick')
const removeWalden = addBook('Walden')
removeWalden()
console.log(listBooks()) // ["Moby Dick"]
Note that I have one object which is mutated: books.
My question is, how can I refactor this code so that books is immutable yet I achieve the same end-result. Feel free to use a functional library like Ramda if need be. My naive thought here is somehow use recursion to pass in a new value of books and then pass that version back. Seems a bit overreach so I thought to seek out help from someone more knowledgable in this arena.
Thanks for your insight!
Just leave book constant in your bookshelf. That will require creating a new bookshelf every time of course, so the easiest approach is to make books be a parameter of the function:
function bookShelf(books) {
return {
listBooks() { return books },
addBook(book) { return bookShelf(books.concat([book])); }
}
}
const empty = bookShelf([]);
const shelfWithMobyDick = empty.addBook('Moby Dick');
const shelfWithMobyDickAndWalden = shelfWithMobyDick.addBook('Walden');
console.log(shelfWithMobyDick.listBooks());
As you can see, there's no need for a removeBook function - you just use the old value that had not yet included the book.
If you want to be able to remove the book you just added from an arbitrary bookshelf, you can also return both the new bookshelf and a remover function:
…,
addBook(book) {
return {
bookShelf: bookShelf(books.concat([book]));
removeThis(shelf) { return bookShelf(shelf.listBooks().filter(b => b !== book)); }
};
}
to be used as
const empty = bookShelf([]);
const {bookShelf: shelfWithMobyDick, removeThis: removeMobyDick} = empty.addBook('Moby Dick');
const {bookShelf: shelfWithMobyDickAndWalden, removeThis: removeWalden} = shelfWithMobyDick.addBook('Walden');
const shelfWithWalden = removeMobyDick(shelfWithMobyDickAndWalden);
console.log(shelfWithWalden.listBooks());
The bookshelf type doesn’t really seem to be accomplishing anything here, so let’s just make it a list (array).
let bookshelf = [];
Now it looks like you want a way to produce a list with a new item and a way of removing that item from the list. A little weird, but you can do that by returning both in a tuple (array):
const remove = (list, value) =>
list.filter(x => x !== value);
const addRemovable = (list, value) =>
[[...list, value], list => remove(list, value)];
let bookshelf = [];
let removeMobyDick;
let removeWalden;
[bookshelf, removeMobyDick] = addRemovable(bookshelf, 'Moby Dick');
[bookshelf, removeWalden] = addRemovable(bookshelf, 'Walden');
bookshelf = removeWalden(bookshelf);
console.log(bookshelf);
This doesn’t look nice and you probably wouldn’t want to write something like it, but it does achieve the same thing as your original.
Here's a simple test I've written of what I want to do with an immutable object
it('adds a new map with loaded data where the key is the ticker symbol', () => {
const state = Map();
const tickers = List.of('AAPL', 'TSLA', 'GOOGL');
const nextState = addTickerKeys(state, tickers);
expect(nextState).to.equal(fromJS({
tickers: ['AAPL', 'TSLA', 'GOOGL'],
data: {
AAPL: {},
TSLA: {},
GOOGL: {}
}
}));
})
How do I add the data object and the corresponding keys with empty data into the state object?
Here is what I have tried so far
export function addTickerKeys(state, tickers) {
const newState = setTickers(state, tickers);
const tickerList = newState.get('tickers');
return tickerList.forEach((value, key, iter) => {
return newState.setIn(['data', key]);
})
}
I've tried substituting value, key and iter in place of return newState.setIn(['data', key]) as per the docs (https://facebook.github.io/immutable-js/docs/#/Map/set)
However, I get the same response back each time,
AssertionError: expected 3 to equal { Object (size, _root, ...) }
Can someone explain to me what's going wrong? This seems a simple enough task but I seem to be struggling with Immutable objects and the documentation in TypeScript doesn't help.
Here's a quick answer to this, that I just figured out.
Immutable does not seem to have a great inbuilt function for this task and the way you have to return the state from a pure function is just frustrating.
Here's a quick and dirty solution to adding the object key value pairs.
export function addTickerKeys(state) {
const tickerArray = state.get('tickers');
let newState = Map();
for(let i = 0; i < tickerArray.size; i++){
ctr++;
newState = state.setIn(['data', tickerArray.get(i)], Map());
state = state.concat(newState);
if(i === tickerArray.size - 1){
return state;
}
}
}
If anyone else still has a different answer, a more elegant inbuilt solution perhaps do share.
A quick comment on your first solution:
Much like the native forEach method on Array.prototype, the forEach method on immutable List types is used for executing some side effect upon each iteration of the List. One subtle difference between the two, however, is when the callback function returns false on the immutable forEach, it will immediately end execution of the loop, whereas the native forEach is interminable. Here, you utilize the forEach method on Lists but are returning a value, suggesting that you might have confused it with the map method on immutable types and arrays. Unfortunately, map is not what you're looking for either.
Now, another solution:
function addTickerKeys(state, tickers) {
return tickers.reduce((acc, ticker) => {
return acc.setIn([ 'data', ticker ], Map());
}, Map({
tickers: List(tickers),
}))
}
I am trying to find the best way to remove an element from an array in the state of a component. Since I should not modify the this.state variable directly, is there a better way (more concise) to remove an element from an array than what I have here?:
onRemovePerson: function(index) {
this.setState(prevState => { // pass callback in setState to avoid race condition
let newData = prevState.data.slice() //copy array from prevState
newData.splice(index, 1) // remove element
return {data: newData} // update state
})
},
Thank you.
updated
This has been updated to use the callback in setState. This should be done when referencing the current state while updating it.
The cleanest way to do this that I've seen is with filter:
removeItem(index) {
this.setState({
data: this.state.data.filter((_, i) => i !== index)
});
}
You could use the update() immutability helper from react-addons-update, which effectively does the same thing under the hood, but what you're doing is fine.
this.setState(prevState => ({
data: update(prevState.data, {$splice: [[index, 1]]})
}))
I believe referencing this.state inside of setState() is discouraged (State Updates May Be Asynchronous).
The docs recommend using setState() with a callback function so that prevState is passed in at runtime when the update occurs. So this is how it would look:
Using Array.prototype.filter without ES6
removeItem : function(index) {
this.setState(function(prevState){
return { data : prevState.data.filter(function(val, i) {
return i !== index;
})};
});
}
Using Array.prototype.filter with ES6 Arrow Functions
removeItem(index) {
this.setState((prevState) => ({
data: prevState.data.filter((_, i) => i !== index)
}));
}
Using immutability-helper
import update from 'immutability-helper'
...
removeItem(index) {
this.setState((prevState) => ({
data: update(prevState.data, {$splice: [[index, 1]]})
}))
}
Using Spread
function removeItem(index) {
this.setState((prevState) => ({
data: [...prevState.data.slice(0,index), ...prevState.data.slice(index+1)]
}))
}
Note that in each instance, regardless of the technique used, this.setState() is passed a callback, not an object reference to the old this.state;
Here is a way to remove the element from the array in the state using ES6 spread syntax.
onRemovePerson: (index) => {
const data = this.state.data;
this.setState({
data: [...data.slice(0,index), ...data.slice(index+1)]
});
}
I want to chime in here even though this question has already been answered correctly by #pscl in case anyone else runs into the same issue I did. Out of the 4 methods give I chose to use the es6 syntax with arrow functions due to it's conciseness and lack of dependence on external libraries:
Using Array.prototype.filter with ES6 Arrow Functions
removeItem(index) {
this.setState((prevState) => ({
data: prevState.data.filter((_, i) => i != index)
}));
}
As you can see I made a slight modification to ignore the type of index (!== to !=) because in my case I was retrieving the index from a string field.
Another helpful point if you're seeing weird behavior when removing an element on the client side is to NEVER use the index of an array as the key for the element:
// bad
{content.map((content, index) =>
<p key={index}>{content.Content}</p>
)}
When React diffs with the virtual DOM on a change, it will look at the keys to determine what has changed. So if you're using indices and there is one less in the array, it will remove the last one. Instead, use the id's of the content as keys, like this.
// good
{content.map(content =>
<p key={content.id}>{content.Content}</p>
)}
The above is an excerpt from this answer from a related post.
Happy Coding Everyone!
As mentioned in a comment to ephrion's answer above, filter() can be slow, especially with large arrays, as it loops to look for an index that appears to have been determined already. This is a clean, but inefficient solution.
As an alternative one can simply 'slice' out the desired element and concatenate the fragments.
var dummyArray = [];
this.setState({data: dummyArray.concat(this.state.data.slice(0, index), this.state.data.slice(index))})
Hope this helps!
You can use this function, if you want to remove the element (without index)
removeItem(item) {
this.setState(prevState => {
data: prevState.data.filter(i => i !== item)
});
}
You could make the code more readable with a one line helper function:
const removeElement = (arr, i) => [...arr.slice(0, i), ...arr.slice(i+1)];
then use it like so:
this.setState(state => ({ places: removeElement(state.places, index) }));
Just a suggestion,in your code instead of using let newData = prevState.data you could use spread which is introduced in ES6 that is you can uselet newData = ...prevState.data for copying array
Three dots ... represents Spread Operators or Rest Parameters,
It allows an array expression or string or anything which can be iterating to be expanded in places where zero or more arguments for function calls or elements for array are expected.
Additionally you can delete item from array with following
onRemovePerson: function(index) {
this.setState((prevState) => ({
data: [...prevState.data.slice(0,index), ...prevState.data.slice(index+1)]
}))
}
Hope this contributes!!
In react setState
const [array, setArray] = useState<any>([]);
//element you want to remove
let temp = array.filter((val: number) => {
return val !== element;
});
setArray(temp);
Here is a simple way to do it:
removeFunction(key){
const data = {...this.state.data}; //Duplicate state.
delete data[key]; //remove Item form stateCopy.
this.setState({data}); //Set state as the modify one.
}
Hope it Helps!!!