How to dynamically change object values of a state after a callback in React? - reactjs

Rookie question.
Consider the code snippet below for a second:
export const Parent = ({data}) => {
const [myData, setShowResults] = useState({isOpen: false, title: 'test'})
function handleCheck(evt) {
console.log(evt.currentTarget.textContent);
//how to change current myData.title with evt.currentTarget.textContent?
}
return (
<div className='c-dropdown'>
<Child data={data} isOpen={myData.isOpen} onCheck={handleCheck}/>
<p>{myData.title}</p>
</div>
);
}
Inside the handleCheck callback function I receive the desired information, yet I cannot find a way how to change myData.title with the newly received information.

Use a functional state update and shallow copy the previous state object.
Example:
function handleCheck(evt) {
const { textContent } = evt.currentTarget;
console.log(textContent);
setShowResults(data => ({
...data, // <-- copy previous state
title: textContent, // <-- update property
}));
}

Related

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

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));
}

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;

Pass function via props cause useEffect infinite loop if I do not destructure props

I have a parent component with a state. And I want to pass a handler to set some state from a child component.
This is my parent component.
function ParentComponent() {
const [filters, setFilters] = useState({});
const setFiltersHandler = useCallback(filtersObj => {
setFilters(filtersObj);
}, []);
useEffect(() => {
// Do something and pass this to <Content /> component
}, [filters]);
return (
<div>
<Content filters={filters}>
<SideBarFilters applyFilters={setFiltersHandler} />
</div>
);
}
And this is my child component. This causes infinit loop.
const SideBarFilters = props => {
const [filterForm, setFilterForm] = useState({
specialities: {value: "all"}
});
// Some code with a input select and the handler to set filterForm
useEffect(() => {
let filterObj = {};
for (let key in orderForm) {
filterObj = updateObject(filterObj, {
[key]: orderForm[key]["value"]
});
}
props.applyFilters(filterObj);
}, [props, orderForm]);
return <OtherComponent />;
};
But if I destructure the props, it does not loop. Like this
const SideBarFilters = ({applyFilters}) => {
// same code as before
useEffect(() => {
// same as before
applyFilters(filterObj);
}, [applyFilters, orderForm]);
return <OtherComponent />;
};
My guess is that has something to do with how React compare props.
Maybe I should memo all props. But I think that is not a pattern
props object is referentially different each time parent re-renders(and re-renders SideBarFilters).
You should not fight that. Trying to find workaround you may run into brand new issues with stale date.
Destructure as you do, it's expected and suggested way to deal with dependencies in hooks.

Calling 'setState' of hook within context sequentially to store data resulting in race condition issues

I've created a context to store values of certain components for display elsewhere within the app.
I originally had a single display component which would use state when these source components were activated, but this resulted in slow render times as the component was re-rendered with the new state every time the selected component changed.
To resolve this I thought to create an individual component for each source component and render them with initial values and only re-render when the source components values change.
i.e. for the sake of an example
const Source = (props) => {
const { name, some_data} = props;
const [setDataSource] = useContext(DataContext);
useEffect(() => {
setDataSource(name, some_data)
}, [some_data]);
return (
...
);
}
const DataContextProvider = (props) => {
const [currentState, setState] = useState({});
const setDataSource = (name, data) => {
const state = {
...currentState,
[name]: {
...data
}
}
}
return (
...
)
}
// In application
<Source name="A" data={{
someKey: 0
}}/>
<Source name="B" data={{
someKey: 1
}}/>
The state of my provider will look like so;
{
"B": {
"someKey": 1
}
}
I believe this is because setState is asynchronous, but I can't think of any other solution to this problem
You can pass the function to setState callback:
setState((state) => ({...state, [name]: data}))
It takes the latest state in argument in any case, so it always safer to use if your update depends on previous state.

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