My component is mutating its props when it shouldn't be - reactjs

I have a component that grabs an array out of a prop from the parent and then sets it to a state. I then modify this array with the intent on sending a modified version of the prop back up to the parent.
I'm confused because as I modify the state in the app, I console log out the prop object and it's being modified simultaneously despite never being touched by the function.
Here's a simplified version of the code:
import React, { useEffect, useState } from 'react';
const ExampleComponent = ({ propObj }) => {
const [stateArr, setStateArr] = useState([{}]);
useEffect(() => {
setStateArr(propObj.arr);
}, [propObj]);
const handleStateArrChange = (e) => {
const updatedStateArr = [...stateArr];
updatedStateArr[e.target.dataset.index].keyValue = parseInt(e.target.value);
setStateArr(updatedStateArr);
}
console.log(stateArr, propObj.arr);
return (
<ul>
{stateArr.map((stateArrItem, index) => {
return (
<li key={`${stateArrItem._id}~${index}`}>
<label htmlFor={`${stateArrItem.name}~name`}>{stateArrItem.name}</label>
<input
name={`${stateArrItem.name}~name`}
id={`${stateArrItem._id}~input`}
type="number"
value={stateArrItem.keyValue}
data-index={index}
onChange={handleStateArrChange} />
</li>
)
})}
</ul>
);
};
export default ExampleComponent;
As far as I understand, propObj should never change based on this code. Somehow though, it's mirroring the component's stateArr updates. Feel like I've gone crazy.

propObj|stateArr in state is updated correctly and returns new array references, but you have neglected to also copy the elements you are updating. updatedStateArr[e.target.dataset.index].keyValue = parseInt(e.target.value); is a state mutation. Remember, each element is also a reference back to the original elements.
Use a functional state update and map the current state to the next state. When the index matches, also copy the element into a new object and update the property desired.
const handleStateArrChange = (e) => {
const { dataset: { index }, value } = e.target;
setStateArr(stateArr => stateArr.map((el, i) => index === i ? {
...el,
keyValue: value,
} : el));
}

Related

Why is my component failing to run when I call it?

I am struggling to find why my component is not responding to being called by its parent. I am trying to integrate Cloud Firestore with code that previously ran using Redux. My first goal is to populate my List with data from Firestore.
Here are my (simplified) components in question:
// List.js
import React, { useEffect, useState } from "react";
import db from "../../db";
import { onSnapshot, query, collection, orderBy } from "firebase/firestore";
import TaskItem from "./TaskItem";
const List = () => {
const [taskList, setTaskList] = useState([]); // Currently assumes DB never empty, populates on initial render
const [isInitialRender, setIsInitialRender] = useState(true);
// Firestore
const ref = collection(db, "Tasks");
const q = query(ref, orderBy("listIndex"));
useEffect(() => {
// Execute only on initial render
if (isInitialRender) {
// Populate task list
onSnapshot(q, (querySnapshot) => {
setTaskList(() => querySnapshot.docs)
}, (error) => {
console.log(error)
})
};
setIsInitialRender(() => false);
}, []);
return (
<>
<h2>List</h2>
{taskList.forEach((task) => ( // console-logging `task` here will output correct data
<ul key={task.data().key}>
<TaskItem
id={task.data().key}
// docRef={taskDoc}
/>
</ul>
))
}
</>
);
};
export default List;
// TaskItem.js
import React from "react";
const TaskItem = (props) => {
console.log('This will not print')
const submitHandler = () => console.log('Submitted');
return (
<form onSubmit={submitHandler}>
<input
autoFocus
type="text"
/>
</form>
);
};
export default TaskItem;
I have tried:
Populating the state with the data from each document (rather than assigning it directly), then passing the contents as props. This led to (I believe) an infinite loop, and ideally I would like to pass the actual DocumentReference to the TaskItem anyways. So this was a bust for me.
Returning [...querySnapshot.docs], or even (prev) => prev = [...querySnapshot.docs] in the state setter. No response from TaskItem().
Decomposing the taskList state into a new dummy array, and using that array to populate the props for TaskItem.
I know that the task data is being fetched successfully because I can satisfactorily log taskList's contents from the map function in List's return statement. But it seems like TaskItem() never runs.
Does anyone see my error here?
edit: sorry I assumed you were using map. I'm not sure why your forEach isn't working but map would work, from my example
edit 2: you probably are looking to use map because you want to transform every element in the array: JavaScript: Difference between .forEach() and .map()
you forgot to return something from the map, and maybe need {} instead.
try
{taskList.forEach((task) => {
return (
<ul key={task.data().key}>
<TaskItem
id={task.data().key}
// docRef={taskDoc}
/>
</ul>
)
})

Ract functional component doesn't update after state change

I was trying to update the state after user selects the dropdown, however, the selected option is never changed. See the gif -- https://recordit.co/KH2Pqn34bp.
I am confused that ideally, after using setFilterOptions to update state, it's supposed to re-render this component with a new value, but it doesn't happen. Could anyone help take a look? What am I missing here? Thanks a lot!
Example code on sandbox -- https://codesandbox.io/s/react-select-default-value-forked-1ybdk?file=/index.js
const SearchFilter = () => {
const [filterOptions, setFilterOptions] = useContext(SearchFilterContext);
let curSort = filterOptions['sortType'] || DEFAULT_SORT_OPTION;
const handleSortChange = (option) => {
setFilterOptions(previous => Object.assign(previous, { 'sortType': option }))
};
return (
<span className='filter-container'>
<Select options={SORT_TYPE_OPTIONS} value={curSort} onChange={handleSortChange}/>
</span>
);
};
Couple of problems in your code:
To set the default value of the Select component, you have written some unnecessary code. Instead, you could just use the defaultValue prop to set the default value of the Select component.
<Select
options={OPTIONS}
defaultValue={OPTIONS[0]}
onChange={handleSortChange}
/>
You are mutating the state directly. Object.assign(...) returns the target object. In your case, the target object is the previous state.
Instead of returning the new state object, you mutate the state directly and return the previous state object which prevents a re-render.
Using the spread-syntax, you can update the state correctly as shown below:
const handleSortChange = (option) => {
setFilterOptions({ ...filterOptions, sortType: option });
};
Following code fixes the above mentioned problem in your component:
const SearchFilter = () => {
const [filterOptions, setFilterOptions] = useState({});
const handleSortChange = (option) => {
setFilterOptions({ ...filterOptions, sortType: option });
};
return (
<span className="filter-container">
<Select
options={OPTIONS}
defaultValue={OPTIONS[0]}
onChange={handleSortChange}
/>
</span>
);
};
The reason you're not seeing a change is that this functional component will only re-render when it sees that your state changed (based on your sandbox script). At the moment when you use Object.assign(previous, { 'sortType': option}) you're changing the sortType property in the object but the object itself doesn't change and so the functional component doesn't see a change.
We can resolve this by using either Object.assign({}, previous, { 'sortType': option}) which will create a NEW object with the previous state attributes and the changed sortType (the first param to Object.assign is the object where the following object properties will get copied into. if we use an empty object that's the equivalent of a new object) or we can use a spread operator and replace it with ({...sortType, 'sortType': option}) which will also create a new object that the functional component will recognize as a changed state value.
const handleSortChange = (option) => {
setFilterOptions(previous => Object.assign({}, previous, { 'sortType': option }))
};
or
const handleSortChange = (option) => {
setFilterOptions(previous => ({...previous, 'sortType': option})
};
Keep in mind these are shallow object copies.

React useState, value of input doesnt update

I wish to generate inputs based on json, so first I set it to initial state, then in child componenet I want to modify it's field, thing is that component doesnt update... It renders once and have no idea how to make it be updated each time when input onChange change it's value. Any idea how to make value of input be updated each time when I type something?
PARENT
function App() {
const [inputValue, setInputValue] = useState(chunkingRow);
const handleChunkingChange = (e, index) => {
let inputContent = inputValue;
const reg = new RegExp(/^[0-9]+$/);
if (e.target.value.match(reg)) {
inputContent[index] = {
...inputContent[index],
content: e.target.value,
};
setInputValue(inputContent);
console.log(inputValue);
} else console.log('not a number')
};
return (
<div>
<Wrapper>
{Chunk(inputValue, handleChunkingChange)}
</Wrapper>
</div>
);
}
CHILD
const Chunk = (inputValue, handleChunkingChange) => {
return(
<div>
{inputValue.map((el, index) => (
<div key={index}>
<p>{el.title}</p>
{console.log(el.content)}
<input
type="text"
onChange={(e, i) => handleChunkingChange(e, index)}
value={el.content}
/>
</div>
))}
</div>
);
}
link to demo
https://codesandbox.io/s/stoic-mirzakhani-46exz?file=/src/App.js
Not completely sure why this happens, but probably because of the way you handle the input change. It seems to me that component doesn't recognize that array changed. How I managed to fix your code is replacing line 9 in App component with following code:
let inputContent = [...inputValue];
By doing that, array's reference is changed and components are updated.
Just update your code as follow:
let inputContent = [ ...inputValue ];
You are mutating the state object.
let inputContent = inputValue;
That's why the state is not re-rendered. Change it to
let inputContent = [...inputValue];
An example of mutating objects. React compares previous state and current state and renders only if they are different.
const source = { a: 1, b: 2 };
const target = source;
console.log(target);
console.log(target === source); = true
target.b = 99;
console.log({target});
console.log({source}); //source == target due to mutation
console.log(source === target); = true
Remember, never mutate.

Using React hooks, how can I update an object that is being passed to a child via props?

The parent component contains an array of objects.
It maps over the array and returns a child component for every object, populating it with the info of that object.
Inside each child component there is an input field that I'm hoping will allow the user to update the object, but I can't figure out how to go about doing that.
Between the hooks, props, and object immutability, I'm lost conceptually.
Here's a simplified version of the parent component:
const Parent = () => {
const [categories, setCategories] = useState([]);
useEffect(()=>{
// makes an axios call and triggers setCategories() with the response
}
return(
categories.map((element, index) => {
return(
<Child
key = {index}
id = {element.id}
firstName = {element.firstName}
lastName = {element.lastName}
setCategories = {setCategories}
})
)
}
And here's a simplified version of the child component:
const Child = (props) => {
return(
<h1>{props.firstName}</h1>
<input
defaultValue = {props.lastName}
onChange={()=>{
// This is what I need help with.
// I'm a new developer and I don't even know where to start.
// I need this to update the object's lastName property in the parent's array.
}}
)
}
Maybe without knowing it, you have lifted the state: basically, instead of having the state in the Child component, you keep it in the Parent.
This is an used pattern, and there's nothing wrong: you just miss a handle function that allows the children to update the state of the Parent: in order to do that, you need to implement a handleChange on Parent component, and then pass it as props to every Child.
Take a look at this code example:
const Parent = () => {
const [categories, setCategories] = useState([]);
useEffect(() => {
// Making your AXIOS request.
}, []);
const handleChange = (index, property, value) => {
const newCategories = [...categories];
newCategories[index][property] = value;
setCategories(newCategories);
}
return categories.map((c, i) => {
return (
<Child
key={i}
categoryIndex={i}
firstName={c.firstName}
lastName={c.lastName}
handleChange={handleChange} />
);
});
}
const Child = (props) => {
...
const onInputChange = (e) => {
props.handleChange(props.categoryIndex, e.target.name, e.target.value);
}
return (
...
<input name={'firstName'} value={props.firstName} onChange={onInputChange} />
<input name={'lastName'} value={props.lastName} onChange={onInputChange} />
);
}
Few things you may not know:
By using the attribute name for the input, you can use just one handler function for all the input elements. Inside the function, in this case onInputChange, you can retrieve that information using e.target.name;
Notice that I've added an empty array dependecies in your useEffect: without it, the useEffect would have run at EVERY render. I don't think that is what you would like to have.
Instead, I guest you wanted to perform the request only when the component was mount, and that is achievable with n empty array dependecies;

Filtering an icon from an array of icon strings for re-render

I'm trying to take an e.target.value which is an icon and filter it out from an array in state, and re-render the new state minus the matching icons. I can't seem to stringify it to make a match. I tried pushing to an array and toString(). CodeSandbox
✈ ["✈", "♘", "✈", "♫", "♫", "☆", "♘", "☆"]
Here is the code snippet (Parent)
removeMatches(icon) {
const item = icon;
const iconsArray = this.props.cardTypes;
const newIconsArray =iconsArray.filter(function(item) {
item !== icon
})
this.setState({ cardTypes: newIconsArray });
}
This is a function in the parent component Cards, when the child component is clicked I pass a value into an onClick. Below is a click handler in the Child component
handleVis(e) {
const item = e.target.value
this.props.removeMatches(item)
}
First of all, there's nothing really different about filtering an "icon" string array from any other strings. Your example works like this:
const icons = ["✈", "♘", "✈", "♫", "♫", "☆", "♘", "☆"]
const icon = "✈";
const filteredIcons = icons.filter(i => i !== icon);
filteredIcons // ["♘", "♫", "♫", "☆", "♘", "☆"]
Your CodeSandbox example has some other issues, though:
Your Card.js component invokes this.props.removeMatches([item]) but the removeMatches function treats the argument like a single item, not an array.
Your Cards.js removeMatches() function filters this.props.cardTypes (with the previously mentioned error about treating the argument as a single item not an array) but does not assign the result to anything. Array.filter() returns a new array, it does not modify the original array.
Your Cards.js is rendering <Card> components from props.cardTypes, this means that Cards.js is only rendering the cards from the props it is given, so it cannot filter that prop from inside the component. You have a few options:
Pass the removeMatches higher up to where the cards are stored in state, in Game.js as this.state.currentCards, and filter it in Game.js which will pass the filtered currentCards back down to Cards.js.
// Game.js
removeMatches = (items) => {
this.setState(prevState => ({
currentCards: prevState.currentCards.filter(card => items.indexOf(card) == -1)
}));
}
// ...
<Cards cardTypes={this.state.currentCards} removeMatches={this.removeMatches} />
// Cards.js
<Card removeMatches={this.props.removeMatches}/>
// Card.js -- same as it is now
Move Cards.js props.cardTypes into state (ex state.currentCards) within Cards.js, then you can filter it out in Cards.js and render from state.currentCards instead of props.cardTypes. To do this you would also need to hook into componentWillReceiveProps() to make sure that when the currentCards are passed in as prop.cardTypes from Game.js that you update state.currentCards in Cards.js. That kind of keeping state in sync with props can get messy and hard to follow, so option 1 is probably better.
// Cards.js
state = { currentCards: [] }
componentWillReceiveProps(nextProps) {
if (this.props.cardTypes !== nextProps.cardTypes) {
this.setState({ currentCards: nextProps.cardTypes });
}
}
removeMatches = (items) => {
this.setState(prevState => ({
currentCards: prevState.currentCards.filter(card => items.indexOf(card) == -1)
}));
}
render() {
return (
<div>
{ this.state.currentCards.map(card => {
// return rendered card
}) }
</div>
);
}
Store all the removed cards in state in Cards.js and filter cardTypes against removedCards before you render them (you will also need to reset removedCards from componentWillReceiveProps whenever the current cards are changed):
// Cards.js
state = { removedCards: [] }
componentWillReceiveProps(nextProps) {
if (this.props.cardTypes !== nextProps.cardTypes) {
this.setState({ removedCards: [] });
}
}
removeMatches = (items) => {
this.setState(prevState => ({
removedCards: [...prevState.removedCards, ...items]
}));
}
render() {
const remainingCards = this.props.cardTypes.filter(card => {
return this.state.removedCards.indexOf(card) < 0;
});
return (
<div>
{ remainingCards.map(card => {
// return rendered card
})}
</div>
);
}
As you can see, keeping state in one place in Game.js is probably your cleanest solution.
You can see all 3 examples in this forked CodeSandbox (the second 2 solutions are commented out): https://codesandbox.io/s/6yo42623p3

Resources