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),
}))
}
Related
a redux noob here.
In redux tutorial in this part particularly Why they didn't do something like that
case 'todos/todoToggled': {
return {
...state,
todos: state.todos.map(todo => {
if (todo.id === action.payload.todoId) {
todo.completed = !todo.completed
}
return todo
}
)
}
As far as I know map() doesn't have any side effects quoted from Mozilla docs
The map() method creates a new array populated with the results of calling a provided function on every element in the calling array.
Why they did this extra step?
// We've found the todo that has to change. Return a copy:
return {
...todo,
// Flip the completed flag
completed: !todo.completed
}
Will it affect the functionality? or they just want to be consistent?
As far as I know map() doesn't have any side effects
It can have side-effects, that fact that the map method creates a new array doesn't mean it can't have any side effects. For example:
const arr = [{x: 1}, {x: 2}, {x: 3}];
const newA = arr.map(obj => {
obj.x += 1; // changes the objects in `arr`
return obj;
});
console.log(newA); // new array (objects are still references to original ones)
console.log(arr); // Also changed!
The map function gets passed a reference to each object, meaning that when you change obj, it also changes it within the original array. As a result, when you do:
todo.completed = !todo.completed
you're changing the todo objects in your array, which results in you changing your state directly.
Unlike the above, the presented code:
{
...todo,
completed: !todo.completed
}
does not change the todo object reference, but rather, it takes all the (enumerable own) keys from it and puts it in a new object, which also overwrites the completed key to hold the negated (ie: opposite) completed boolean value.
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.
Can someone explain the part in the exports section, I seem to lost and stuck for a while. Starting from importPromise. It seems like there's a lot going on, such as arrow functions and map method. I can't see where the data flows from where to where.
const keystone = require('keystone');
const PostCategory = keystone.list('PostCategory');
const Post = keystone.list('Post');
const importData = [
{ name: 'A draft post', category: 'Keystone JS' },
...
];
exports = function (done) {
const importPromise = importData.map(({ name, category }) => createPost({ name, category }));
importPromise.then(() => done()).catch(done);
};
const categories = {};
const createPost = ({ name, category }) => {
let postCategory = new PostCategory.model({ category });
if (categories[category]) {
postCategory = categories[category];
}
categories[category] = postCategory;
const post = new Post.model({ name });
post.category = postCategory._id.toString();
return Promise.all([
post.save(),
postCategory.save()
]);
}
Quite some ES6 magic involved :)
const importPromise = importData.map(({ name, category }) => createPost({ name, category }));
importdata is an array. What the map function on an array does is to take every item of the array and apply a function to it, then return a new array with all of the items in the original array, but modified. map function
Instead of writing .map(function(item) { ... } the preferred way of writing this in ES6 is with a fat arrow function, i.e. .map((item) => ...
The third bit of magic is called destructuring assignment. What it does is it takes an object in this case, and assigns the obj.name and obj.category to two new variables name and category. We can use those variables within our function as if we had called the function with two separate arguments.
Now remember our map function dictates we write a function that takes an array item as a parameter, and returns the modified item. So what we end up with is a map function looping through the arguments of importData, taking name and category of each item and calling another function createPost with them. The result of createPost is the new value of the item, and it all gets appended to an array of the same size as the old one, with the modified items.
importPromise.then(() => done()).catch(done);
createPost creates a promise out of each item. You can read more about Promise here. The .then method on a Promise takes functions as its argument; to be called when the promise returns (either with success or an error). The () => done() is simply a function in fat arrow syntax that takes no arguments and calls the done function. .catch takes a function as well (done is a function), and is executed when the promise returns an error. NB. this way the done function is called both on success and on error!
--and no, this code won't work because what we're creating on the first line with importPromise is not actually a promise, but an array of promises!
Good luck with reading up, and as Beri suggests it might be worthwhile translating the code to es5 to follow along.
I don't know much about KeystoneJS. Anyway, here are my two cents:
const importData = [
{ name: 'A draft post', category: 'Keystone JS' },
// ...
];
importData is an Array which holds a bunch of Object instances, each having a name and category key with String values. For me, it appears this is some "mock data", which is simply put there for testing purposes.
I shifted the next parts, because it makes the code more understandable.
This part:
const categories = {};
Looks to me like the person who wrote it tried to implement some form of "caching". The categories constant is a mere "container" to store posts in so they can be reused later instead of being recreated. The createPost function reveals the purpose of it if you read through it.
const createPost = ({ name, category }) => {
let postCategory = new PostCategory.model({ category });
if (categories[category]) {
postCategory = categories[category];
}
categories[category] = postCategory;
const post = new Post.model({ name });
post.category = postCategory._id.toString();
return Promise.all([
post.save(),
postCategory.save()
]);
}
The first if seems to be there to make use of the "caching" construct (const category), but the way it does it is a bit confusing. Here's how I'd refactor it:
const createPost = ({ name, category }) => {
if (!categories[category]) {
categories[category] = new PostCategory.model({ category });;
}
const post = new Post.model({ name });
post.category = categories[category]._id.toString();
return Promise.all([
post.save(),
categories[category].save()
]);
}
Finally for the exports part:
The module exports a function which awaits a callback as argument (done). It then tries to create a Promise from all the "posts" of the mock data (and - to my understanding - fails), by mapping the createPost function over it. The reason I think it fails is because Array.prototype.map doesn't return a Promise, it returns a new Array instance which doesn't have a then method (see next line). Instead of calling then, it should be a Promise.all again. When that final Promise succeeds (or fails), the callback is called with the result.
exports = function (done) {
const importPromise = importData.map(({ name, category }) => createPost({ name, category }));
importPromise.then(() => done()).catch(done);
};
Again, I'd rewrite it this way:
exports = function (done) {
Promise.all(importData.map(createPost)).then(done).catch(done);
};
Or just return the final Promise and get rid of the done callback altogether.
https://babeljs.io/repl
You can use this tool to translate.
I've encountered a problem with the immutability of the state. Briefly, the Component's state has "persons" array of objects, each of them has a name property, which value I can update on onChange event.
Here is the updater
nameChangedHandler = (event, id) => {
const value = event.target.value;
const persons = this.state.persons.map((pers) => {
if (pers.id === id) {
pers.name = value;
}
return pers;
});
this.setState((prevState) => {
console.log(prevState);
return {
persons
}
});
}
This code works well for me and updates name property of a certain person, but I noticed that this.setState function mutates previous state.
I'm guessing that the reason may be in the map function. But how can it be? As far as I know, map takes the previos array and returns a new one. And this new array is later assigned to the persons property as a whole new array in setState function
.map creates a new array, but the object inside the array are still reference to the state's object.
One approach is to shallow copy the state's object to the new array:
const persons = this.state.persons.map( pers => {
// Do the shallow copy
if (pers.id === id) {
return {
...pers,
name: value
};
}
// Return the reference, because we don't do any mutations
return pers;
});
The above approach would work correctly, only if the Person objects are primitive values. Otherwise, you should do a deep copy.
Also you can check Immutable.js for safer dealing with immutable processing.
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!!!