I'm sure my issues is around useCallback and the setter method I have here setSpeakers returned from useState but I'm not understanding exactly why my example, as listed here and in codesandbox is not incrementing the state correctly. The old state keeps being returned, not the new incremented state. When I pass a function into setSpeakers instead of just a new state, my example works (you can see the commented out code marked THIS CODE WORKS below the non-working code.
I know others have written articles on this and I've read those articles but still don't get it.
sandbox: https://codesandbox.io/s/elated-lichterman-7rejs?file=/pages/index.js
import React, { useState, memo, useCallback } from "react";
//const Speaker = React.memo(({ imageSrc, counter, setCounter }) => {
const Speaker = ({ speaker, speakerClick }) => {
console.log(speaker.id);
return (
<div>
<span
onClick={() => {
speakerClick(speaker.id);
}}
src={`speakerimages/Speaker-${speaker.id}.jpg`}
width={100}
>
{speaker.id} {speaker.name}
</span>
<span className="fa fa-star "> {speaker.clickCount}</span>
</div>
);
};
function SpeakerList({ speakers, setSpeakers }) {
return (
<div>
{speakers.map((speaker) => {
return (
<Speaker
speaker={speaker}
speakerClick={useCallback((id) => {
// THIS CODE FAILS BECAUSE OF STALE STATE BUT I DON'T GET WHY
const speakersNew = speakers.map((speaker) => {
return speaker.id === id
? { ...speaker, clickCount: speaker.clickCount + 1 }
: speaker;
});
setSpeakers(speakersNew);
// THIS CODE WORKS
// setSpeakers(function(speakers) {
// const speakersNew = speakers.map((speaker) => {
// return speaker.id === id
// ? { ...speaker, clickCount: speaker.clickCount + 1 }
// : speaker;
// });
// return speakersNew;
// });
}, [])}
key={speaker.id}
/>
);
})}
</div>
);
}
//
const App = () => {
const speakersArray = [
{ id: 1124, name: "aaa", clickCount: 0 },
{ id: 1530, name: "bbb", clickCount: 0 },
{ id: 10803, name: "ccc", clickCount: 0 }
];
const [speakers, setSpeakers] = useState(speakersArray);
return (
<div>
<h1>Speaker List</h1>
<SpeakerList speakers={speakers} setSpeakers={setSpeakers}></SpeakerList>
</div>
);
};
export default App;
That's because set state works asynchronously. In the first code that isn't running you set the state and try to immediately have it show up which wont work since there is delay while the state is being set. The second one works because you're passing a callback to set state which basically means set the state and do this while you're at it. while the first one means set the state and then do this afterwards. That's not what you want. Its a loose analogy but look into how and why set state is aync you'll have a better idea. Cheers.
Related
Playing around with React, what is the logic behind the reassignment of variables not changing. For example when creating a var didClick assigning it to false and then changing the var didClick to true when we click on the display button my component still does not list out all the task text.
function App() {
/*var didClick = false*/
var [didClick,setdidClick] = useState(false);
const [tasks,setTasks] = useState([
{
id: 1,
text:'Doctors Appointment',
day:'Feb 5th at 2:30pm',
},
{
id: 2,
text:'Cat Appointment',
day:'Feb 5th at 4:30pm',
},
{
id: 3,
text:'Dog Appointment',
day:'Feb 5th at 7:30pm',
}
])
const functiontoClick = () => {
/*didClick = true*/
setdidClick(true)
}
return (
<div>
<button onClick={()=> functiontoClick()}>Display</button>
<div>
{didClick ? tasks.map((task) => (
<li key={task.id}> {task.text} </li>
)) : null }
</div>
</div>
);
}
export default App;
using useState will ensures that the component has been updated and calls for re-rendering of the component in react.
but using normal declaration using var, const and let will not update or re-render the component.
you can see what happened if you add console.log just before the return.
first case using useState:
var [didClick,setdidClick] = useState(false);
...
const functiontoClick = () => {
/*didClick = true*/
setdidClick(true)
}
console.log({ didClick })
return (
...
the console will return false at first render and after clicking will console true and render the component.
second case using var:
var didClick = false;
...
const functiontoClick = () => {
didClick = true
}
console.log({ didClick })
return (
...
the console will return false at first render and after clicking will not console anything because it's not rendering the component again
As pointed out in the comments it is unclear what you are asking for: but for your example, if you wanna toggle the display you can use the following code:
const [didClick,setdidClick] = useState(false);
<button onClick={()=> useState(!didClick)}>Display/Hide</button>
If your code at the moment you are not using the functionality being provided by useState:
I am using the React useState hook to update a list of items. I would like for only the added/updated components to be rendered but everytime the state of of the list changes all the items in list are re-rendered.
I have followed Preventing list re-renders. Hooks version. to solve the re-render issue but it doesn't work
Can someone help me understand, what's wrong with the below code or if this is actually not the right way to do it
function App() {
const [arr, setArr] = useState([])
useEffect(() => {
//getList here returns a list of elements of the form {id: number, name: string}
setArr(getList());
}, [])
const clickHandle = useCallback((e, id) => {
e.preventDefault()
setArr((arr) => {
return [...arr, {
id: id + 100,
name: `test${id+100}`
}]
})
}, [arr])
return (
<div className="App">
{
arr.map((item) => {
return (
<NewComp key={`${item.id}`} item={item} clickHandle={clickHandle} />
);
})
}
</div>
);
}
const NewComp = ({
item,
clickHandle
}) => {
return (
<div>
<button onClick={(e) => clickHandle(e, item.id)}>{item.name}</button>
</div>
);
}
The reason all your NewComp re-render is because your clickHandle function is being recreated whenever there is any change in the state arr.
This happens because you have added arr as a dependency to useCallback. This however is not required.
Once you fix it, you can wrap your NewComp with React.memo to optimize their re-renders. Also you must note that call the render function of a component is different from actually re-rendering it in the DOM.
const clickHandle = useCallback((e, id) => {
e.preventDefault()
setArr((arr) => {
return [...arr, {
id: id + 100,
name: `test${id+100}`
}]
})
}, []);
const NewComp = React.memo({
item,
clickHandle
}) => {
return (
<div>
<button onClick={(e) => clickHandle(e, item.id)}>{item.name}</button>
</div>
);
});
I have this application that has a deprecated lifecycle method:
componentWillReceiveProps(nextProps) {
if (this.state.displayErrors) {
this._validate(nextProps);
}
}
Currently, I have used the UNSAFE_ flag:
UNSAFE_componentWillReceiveProps(nextProps) {
if (this.state.displayErrors) {
this._validate(nextProps);
}
}
I have left it like this because when I attempted to refactor it to:
componentDidUpdate(prevProps, prevState) {
if (this.state.displayErrors) {
this._validate(prevProps, prevState);
}
}
It created another bug that gave me this error:
Invariant Violation: Maximum update depth exceeded. This can happen
when a component repeatedly calls setState inside componentWillUpdate
or componentDidUpdate. React limits the number of nested updates to
prevent infinite loops.
It starts to happen when a user clicks on the PAY NOW button that kicks off the _handlePayButtonPress which also checks for validation of credit card information like so:
UNSAFE_componentWillReceiveProps(nextProps) {
if (this.state.displayErrors) {
this._validate(nextProps);
}
}
_validate = props => {
const { cardExpireDate, cardNumber, csv, nameOnCard } = props;
const validationErrors = {
date: cardExpireDate.trim() ? "" : "Is Required",
cardNumber: cardNumber.trim() ? "" : "Is Required",
csv: csv.trim() ? "" : "Is Required",
name: nameOnCard.trim() ? "" : "Is Required"
};
if (validationErrors.csv === "" && csv.trim().length < 3) {
validationErrors.csv = "Must be 3 or 4 digits";
}
const fullErrors = {
...validationErrors,
...this.props.validationErrors
};
const isValid = Object.keys(fullErrors).reduce((acc, curr) => {
if (fullErrors[curr]) {
return false;
}
return acc;
}, true);
if (isValid) {
this.setState({ validationErrors: {} });
//register
} else {
this.setState({ validationErrors, displayErrors: true });
}
return isValid;
};
_handlePayButtonPress = () => {
const isValid = this._validate(this.props);
if (isValid) {
console.log("Good to go!");
}
if (isValid) {
this.setState({ processingPayment: true });
this.props
.submitEventRegistration()
.then(() => {
this.setState({ processingPayment: false });
//eslint-disable-next-line
this.props.navigation.navigate("PaymentConfirmation");
})
.catch(({ title, message }) => {
Alert.alert(
title,
message,
[
{
text: "OK",
onPress: () => {
this.setState({ processingPayment: false });
}
}
],
{
cancelable: false
}
);
});
} else {
alert("Please correct the errors before continuing.");
}
};
Unfortunately, I do not have enough experience with Hooks and I have failed at refactoring that deprecated lifecycle method to one that would not create trouble like it was doing with the above error. Any suggestions at a better CDU or any other ideas?
You need another check so you don't get in an infinite loop (every time you call setState you will rerender -> component did update -> update again ...)
You could do something like this:
componentDidUpdate(prevProps, prevState) {
if (this.state.displayErrors && prevProps !== this.props) {
this._validate(prevProps, prevState);
}
}
Also I think that you need to call your validate with new props and state:
this._validate(this.props, this.state);
Hope this helps.
componentDidUpdate shouldn't replace componentWillRecieveProps for this reason. The replacement React gave us was getDerivedStateFromProps which you can read about here https://medium.com/#baphemot/understanding-react-react-16-3-component-life-cycle-23129bc7a705. However, getDerivedStateFromProps is a static function so you'll have to replace all the setState lines in _validate and return an object instead.
This is how you work with prevState and hooks.
Working sample Codesandbox.io
import React, { useState, useEffect } from "react";
import "./styles.css";
const ZeroToTen = ({ value }) => {
const [myValue, setMyValue] = useState(0);
const [isValid, setIsValid] = useState(true);
const validate = value => {
var result = value >= 0 && value <= 10;
setIsValid(result);
return result;
};
useEffect(() => {
setMyValue(prevState => (validate(value) ? value : prevState));
}, [value]);
return (
<>
<span>{myValue}</span>
<p>
{isValid
? `${value} Is Valid`
: `${value} is Invalid, last good value is ${myValue}`}
</p>
</>
);
};
export default function App() {
const [value, setValue] = useState(0);
return (
<div className="App">
<button value={value} onClick={e => setValue(prevState => prevState - 1)}>
Decrement
</button>
<button value={value} onClick={e => setValue(prevState => prevState + 1)}>
Increment
</button>
<p>Current Value: {value}</p>
<ZeroToTen value={value} />
</div>
);
}
We have two components, one to increase/decrease a number and the other one to hold a number between 0 and 10.
The first component is using prevState to increment the value like this:
onClick={e => setValue(prevState => prevState - 1)}
It can increment/decrement as much as you want.
The second component is receiving its input from the first component, but it will validate the value every time it is updated and will allow values between 0 and 10.
useEffect(() => {
setMyValue(prevState => (validate(value) ? value : prevState));
}, [value]);
In this case I'm using two hooks to trigger the validation every time 'value' is updated.
If you are not familiar with hooks yet, this may be confusing, but the main idea is that with hooks you need to focus on a single property/state to validate changes.
I'm trying to create my own pagination (without using a package), but I can't get it to work.
I'm wondering if it has something to do with how I'm copying my arrays, but I'm not really sure.
class InsightSearchResults extends Component {
state = {
start: 0,
end: 2,
insightsArrayOriginal: [],
copiedArr: []
}
UNSAFE_componentWillReceiveProps(nextProps) {
if (nextProps.insightList[0]) {
this.setState({
insightsArrayOriginal: nextProps.insightList[0].insights,
copiedArr: nextProps.insightList[0].insights.splice(this.state.start, this.state.end)
})
}
}
clickNext = () => {
let copied = [...this.state.insightsArrayOriginal];
this.setState({
start: this.state.start + 2,
end: this.state.end + 2
}, () => {
this.setState({
copiedArr: copied.splice(this.state.start, this.state.end)
})
})
}
clickPrev = () => {
this.setState({
start: this.state.start - 2 < 0 ? 0 : this.state.start - 2,
end: this.state.end - 2
})
}
render() {
const { copiedArr } = this.state;
return (
<div style={{padding: "1.5rem"}}>
{copiedArr ? copiedArr.map(insight => (
<div>
<Grid className="insight_result_block">
<Col className="insight_results_col2" span="10">
<div>
<h4>Hello</h4>
<p>{insight.insightDesc}</p>
</div>
</Col>
</Grid>
<hr className="bottom_hr_insight" />
</div>
)) : <p>loading...</p> }
<button onClick={this.clickPrev}>Prev</button>
<button onClick={this.clickNext}>Next</button>
</div>
)
}
}
I haven't really worked on the "prev" part yet. I'm just trying to get the "next" to work for now...
There are two problems:
UNSAFE_componentWillReceiveProps is not called on initial render. From the docs:
React doesn’t call UNSAFE_componentWillReceiveProps() with initial
props during mounting. It only calls this method if some of
component’s props may update. Calling this.setState() generally
doesn’t trigger UNSAFE_componentWillReceiveProps().
splice mutates the original array, use slice instead. See this question.
So you can move the content of UNSAFE_componentWillReceiveProps to componentDidMount and componentDidUpdate
componentDidMount() {
this.updateState();
}
componentDidUpdate() {
// check if a change in props has caused the rerender
// or you will get infinite rerenders if one state update causes the next one
if (
this.props.insightList[0] &&
this.props.insightList[0].insights !== this.state.insightsArrayOriginal
) {
this.updateState();
}
}
These functions don't receive a parameter: replace nextProps parameter with this.props; and change all splice occurrences with slice.
updateState() {
if (this.props.insightList[0]) {
this.setState({
insightsArrayOriginal: this.props.insightList[0].insights,
copiedArr: this.props.insightList[0].insights.slice( . // <-here
this.state.start,
this.state.end
)
});
}
}
clickNext = () => {
let copied = [...this.state.insightsArrayOriginal];
this.setState({ start: this.state.start + 2, end: this.state.end + 2 },
() => {
this.setState({
copiedArr: copied.slice(this.state.start, this.state.end) // <- and here
});
}
);
};
Also, based on this code sample alone, you could entirely remove insightsArrayOriginal from your state and use it from props, but this may change if you plan to expand the functionality.
I'm trying to set state as array of objects, but it fails.
I created project using CRA, and using react-hooks for states.
I get data from graphql server using react-apollo-hooks.
I just declared data object in codesandbox, but it doesn't affect my problem.
For every click, set state(array of object) with data(array of object).
const data = {
lists: [
{
id: "1"
},
{
id: "2"
},
{
id: "3"
}
]
};
const Sample = () => {
const [sample, setSample] = useState([]);
const Updator = async () => {
try {
await data.lists.map(list => {
setSample([
...sample,
{
label: list.id,
value: list.id
}
]);
return true;
});
console.log(sample);
} catch (err) {
throw err;
}
};
return (
<div>
<React.Fragment>
<button
onClick={e => {
Updator();
}}
>
Click me
</button>
<p>
<strong>
{sample.map(single => {
return <div>{single.label}</div>;
})}
</strong>
</p>
</React.Fragment>
</div>
);
};
I attached all test code on below.
Here is codesandbox link.
https://codesandbox.io/s/zr50rv7qjp
I expect result of
123
by click, but result is
3
Also, for additional click, expected result is
123
123
And I get
3
3.
When I use setSample(), I expect function something like Array.push(). But it ignores all the previous data expect the last one.
Any helps will be thankful!
state updater does batching and since you are calling the setSample method in map, only your last value is being written in state.
The best solution here is to return data from map and then update the state once like below.
import React, { useState } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const data = {
lists: [
{
id: "1"
},
{
id: "2"
},
{
id: "3"
}
]
};
const Sample = () => {
const [sample, setSample] = useState([]);
const Updator = async () => {
try {
const newData = data.lists.map(list => {
return {
label: list.id,
value: list.id
};
});
setSample([...sample, ...newData]);
} catch (err) {
throw err;
}
};
return (
<div>
<React.Fragment>
<button
onClick={e => {
Updator();
}}
>
Click me
</button>
<p>
<strong>
{sample.map((single, index) => {
return <div key={index}>{single.label}</div>;
})}
</strong>
</p>
</React.Fragment>
</div>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<Sample />, rootElement);
Working Demo
Another solution is to use the callback method to update state but you must avoid calling state updates multiple times.
You're destructing sample which will not have the latest version of itself when you're looping and calling setSample. This is why it only puts 3 in the list of samples, because the last iteration of the map will destruct an empty sample list and add 3.
To make sure you have the newest value of sample you should pass a function to setSample. This function will get the latest version of your state var from react.
setSample((latest) => {
return [
...latest,
{
label: list.id,
value: list.id
}
]
});