I came up with the following questing while I was trying to make a TODO app using React.js
I was trying to make and update functionality for each task. For that, I made a button which on click, calls a function "update" which calls a function passed as a prop defined in App.js, this function iterates through the tasks array find the one to modify and does its thing, then it sets the new state
App.js
function App() {
const [taskItems, settaskItems] = useState([]);
const updateTask = (task,newname) =>{
const findAndModify=()=>{
let tasks = [...taskItems] // Why let task=taskItems doesnt work?
for (let index = 0; index < tasks.length; index++) {
const t = tasks[index];
if (t.name === task.name){
t.name = newname
}
}
return tasks
}
settaskItems(findAndModify())
}
}
My TaskRow component is the following
<TaskRow task={task} key={task.id} toggleTask={toggleTask} updateTask={updateTask}></TaskRow>
Inside the definition of my TaskRow.js component I have the following code
export const TaskRow = (props) => {
const update = () => {
const newname = "hello";
props.updateTask(props.task, newname);
};
return (
<tr key={props.task.id}>
<td>{props.task.name}</td>
<td>
<input
type="checkbox"
checked={props.task.done}
onChange={() => props.toggleTask(props.task)}
/>
</td>
<td>
<button className="btn btn-success mx-1" onClick={update}>
Actualizar
</button>
</td>
</tr>
);
};
the question comes here, if in findAndModify instead of doing let tasks = [...taskItems] I do task=taskItems the description of the updated task does not re-render until another change happens, but if a do a console.log of the data, it indeed, had been modified
why is that? how does the [...] operator differs of a regular variable assignment?
I've researched about destructuring in JS but nobody talks about some kind of difference between the use of this operator and a regular var assignment in React.js
The difference between:
(1) method:
let tasks = [...takItems];
(2) method:
tasks = taksItems
is the behavior of javascript with Objects type (for ex. Array)
when you assign a variable to an array it creates a reference and not a shallow copy.
So if you use the (2) method and change tasks variable, it will be reflected in tasksItems, if you use the (1) method, you will have 2 independence variables!
React indicates to NEVER mutate a state directly, like you did when using the (2) method and treat it as immutable type.
You can read more about it here
Why we should never update React state directly
To expand a little bit more, the spread operator [...takItem] makes a shallow copy, which means that it will be a new copy of the variable, but because it is 'shallow' any nested non-primitive data will all be pointers. E.g.,
const tasks = ['a', 'b', { subtask: 1 }]
const shallowCopy = [...tasks]
shallowCopy[0] = 'A'; // won't change the value of tasks
shallowCopy[2].subtasks = 'C'; // does change the values of tasks
console.log(tasks)
// ['a', 'b', 'C']
In the example above, the third item in the tasks array is non-primitive (also known as "reference" type), so shallowCopy[2] is just a pointer to the same place in memory as tasks[2]. tasks[2] and shallowCopy[2] are both referencing the same piece of data, which is why when you modify shallowCopy[2].subtasks it also modifies tasks[2].subtasks
Related
i'm using redux to manage my state , my initial state in the reducer contains to arrays !
the first one(games) is the one that i want to modify , the second one (InitialGames) is Initial one that i don't want it to be modified !
the problem is i only make changes on the first array ! but when i console my state after the logic ! i see that both arrays got changed ??? which is confusing !
The case that i'm going into is PLAYER_DEAD
My Reducer
import { ADD_GAME, PLAYER_DEAD, PUT_INFOS, RESET_GAME } from "./actions";
const initialState = {
games: [],
InitialGames: [],
};
export default (state = initialState, action) => {
switch (action.type) {
case RESET_GAME:
state.games[action.payload.gameIndex] =
state.InitialGames[action.payload.gameIndex];
console.log(state);
return state;
case ADD_GAME:
return {
games: [...state.games, action.payload.game],
InitialGames: [...state.games, action.payload.game],
};
case PUT_INFOS:
return {
gameInfos: action.gameInfos,
};
case PLAYER_DEAD:
let newGames = state.games;
let newInitialGames = state.InitialGames;
console.log("Before Changings", newGames, newInitialGames);
let newTeam = newGames[action.payload.indexGame].teams[
action.payload.index
].players.splice(0, 1);
console.log(
"After changings",
newGames,
newInitialGames
);
return {games:newGames,InitialGames:newInitialGames};
}
return state;
};
This would be occurring because you're passing the same action.payload.game object to both of your arrays here:
return {
games: [...state.games, action.payload.game],
// same objects ---------^------v
InitialGames: [...state.games, action.payload.game],
}
When you access the .teams array in your PLAYER_DEAD case, you're accessing the same array in memory shared by both games and InitialGames, the same goes for anything within that array, including the .players array within your .teams array's objects. Because you're updating your array in place in a non-immutable way by using .splice(), you end up modifying your state directly and thus modifying the same .players array referenced by both games and InitialGames.
You need to ensure that you don't modify your state in place by using methods like .splice(). For your particular case, you would do something like so:
const newGames = state.games.map((game, i) => i === action.payload.indexGame
? game.teams.map((team, j) => j === action.payload.index
? {...team, players: team.players.slice(1)} // note slice, not splice
: team
)
: game
);
Above, we map your arrays, updating the items when the inndx matches the item to update. When updating the players array, we use .slice() to remove the first item from the array.
Writing immutable code isn't always easy, that's why redux toolkit has built-in support for immer when you use an API such as createSlice() API that will allow you to write code like you've been doing the mutates your state. See here for more info.
What I try to achieve:
I want to update a value in an obj, which is part of the element of array. See the code below will give you better idea.
There is an issue that I update the value of object, via reference, instead of making a copy. This causes the state behave strangely.
I try to change it to making a copy, but I am not sure.
e.g.
const returnObj = {
...objs,
fields: [{name, value}, {name, value}, {name, value_update_this_only}, ...],
};
// This is the current code
export function* onChange(action) {
// get partial state from redux state
const list = yield select((state) => state.list);
let objs = list[action.index];
// * e.g. objs.fields === [{name, value}, {name, value}, ...]
// * basically following, find the correct field and update its value
// * following has problem, beause we change the value of a reference,
// * instead we should make a new copy, so redux can react
objs.fields.map((field) => {
if (field.name === action.fieldName) {
field["value"] = action.fieldValue;
}
return field;
});
// fire to redux reducer
yield put({
type: "UPDATE",
prop: obj,
docIndex: action.index,
});
}
// the problem: I don't know how to do it in destructing manner.
const returnObj = {
...objs,
fields: [],
};
I think rather than try and come up with a single destructuring statement that makes this work, it's easier to digest (and arguably more readable) in smaller steps:
Make a shallow copy of objs; call it copy for now
Recreate fields array and every item within it
For the desired array item, update its value
Set the copy.fields to the array created in 2
// Step 1: Shallow copy
let copy = { ...objs }
// Step 2: Recreate fields and every item
let fields = copy.fields.map((field) => ({
...field
}))
// Step 3: Update value of desired item
fields.forEach((field) => {
if (field.name === action.fieldName)
field.value = action.fieldValue
})
// Step 4: Reassign fields to the copy
copy.fields = fields
Refactoring this, steps 2-4 can be combined into one step without sacrificing that much readability:
let copy = { ...objs }
copy.fields = copy.fields.map((field) => ({
...field,
value: field.name === action.fieldName ? action.fieldValue : field.value,
}))
It's been a long time since I've used redux or sagas, so I'm not sure whether fields needs to be an entirely new array or if just the changed object within fields needs to be new, but the above can be modified to accommodate either need.
I have the following code for one of the CASES in reducers
case CHART_CHANGE: {
const {method,copiedArray} = payload;
let ref = copiedArray
let swapped = bubblesort(copiedArray) //<------here
console.log(ref)
console.log(swapped)
}
}
The question is this, as you can see here I assigned the variable ref before involving the bubblesort function. But when I run the program, ref variable has the same value with the swapped variable. Why is that?
In javascript objects (arrays are objects) are assigned by reference, so copiedArray and ref are referring (pointing) to the same object. If you change the contents of the object (and your bubblesort function seems to sort in place - that is mutate/change the array internally), the changes are visible in all of the variables: copiedArray, ref and swapped.
const copiedArray = [3,2,1];
const ref = copiedArray;
const swapped = copiedArray.sort();
console.log(copiedArray);
console.log(ref);
console.log(swapped);
Let say I have an object with
let product =
{ id:1,
model:2
}
I can reference the id by
const productId = product.id
Or I can also do
const newProductCreated = Object.assign({}, product)
then reference by newProductCreated.id
Which would be the best way for me to reference the id?
If you are working with the Redux/'Flux' methedology, the second method makes sense, it is not how you should be using it.
Assuming you are trying to create a shallow copy of the object, you can simply do this:
const productCopy = { ...product };
// or const productCopy = Object.assign({}, product) if you can't support ES6
From there, you can make respective changes to the properties within productCopy without affecting the original product.
productCopy.model = 3;
// console.log(productCopy) prints out the modified object
// console.log(product) prints out the original object
This will be conform to the immutable update patterns.
I have noticed that I can pass to React a set of nested arrays and it will render the items properly, but when I go this way, it will not complain of missing keys on my elements.
const stuff = 'a,b,c';
// Nested Array
// React is fine with it and automatically assigns keys
// Sample data: [[a, <br />], [b, <br />], [c, <br />]]
const Foo = () => <div>{stuff.split(',').map(itm => [itm, <br />])}</div>;
// Flat Array
// React warns me that I should assign a key to each element in array
// Sample data: [a, <br />, b, <br />, c, <br />]
const Bar = () => <div>{stuff.split(',').map(itm => [itm, <br />]).reduce((a, b) => a.concat(b), [])}</div>;
Sample pen:
https://codepen.io/FezVrasta/pen/NppLPR
Why does it happens? I can't find any reference to "nested arrays" support in React, neither on "auto assigned keys".
You'll notice that even though a warning is printed to the console, React still shows both Foo and Bar in your HTML being generated. React uses unique keys for reconciliations whilst trying to boost rendering performance. You can read more about this on the React reconciliation recursing on children page. Not providing keys means React cannot be as performant as it has been designed to be.
With regards to your question as to why a warning is not output to the console for nested arrays, we have to dive into the source code:
The function which generates the warning is called validateExplicitKey, and lives in the ReactElementValidator.js module.
This function is used in the validateChildKeys in the same module - looking into the source code gives the following, as of React 15.4.2:
function validateChildKeys(node, parentType) {
if (typeof node !== 'object') {
return;
}
if (Array.isArray(node)) { // 1.
for (var i = 0; i < node.length; i++) {
var child = node[i]; // 2.
if (ReactElement.isValidElement(child)) { // 3.
validateExplicitKey(child, parentType);
}
}
} else if (ReactElement.isValidElement(node)) {
// This element was passed in a valid location.
if (node._store) {
node._store.validated = true;
}
} else if (node) {
var iteratorFn = getIteratorFn(node);
// Entry iterators provide implicit keys.
if (iteratorFn) {
if (iteratorFn !== node.entries) {
var iterator = iteratorFn.call(node);
var step;
while (!(step = iterator.next()).done) {
if (ReactElement.isValidElement(step.value)) {
validateExplicitKey(step.value, parentType);
}
}
}
}
}
}
An array of arrays will enter the first code block
the child will be set to child = ["b", Object] (where 'Object' is react's virtual dom representation for the br node we created via JSX)
the array will be run through the function ReactElement.isValidElement:
ReactElement.isValidElement = function (object) {
return typeof object === 'object' && object !== null && object.$$typeof === REACT_ELEMENT_TYPE;
};
with REACT_ELEMENT_TYPE being set as:
var REACT_ELEMENT_TYPE = typeof Symbol === 'function' && Symbol['for'] && Symbol['for']('react.element') || 0xeac7;
The array is an object, and is not null, but it's $$typeof property hasn't been set here, so the check fails.
$$typeof hasn't been set because React only adds this property to elements it creates to identify whether something is a React Element or not. This includes native HTML elements, and not data types.
Hence the ReactElement.isValidElement check fails, and the warning is never shown.
I've been wondering the same thing recently!
From what I've understood in ReactJS's official docs about how the keys work, I would expect to get the same warning with the nested array of data, just like the one with the flat array of data, since in both cases there are no key attributes set.
Keys help React identify which items have changed, are added, or are removed. Keys should be given to the elements inside the array to give the elements a stable identity.
I actually filled in a bug report (issue) in the ReactJS official GitHub repo, describing the same case you pointed out, but simplified (without the fancy .map() and .reduce() thingies).
It looks like a bug to me.
PS: I will update my answer as soon as the React team responds to me.