Why is setState is not working as expected - reactjs

For some reason my setState isn't updating...My callback isn't even firing off. I tried defining the function onSubmit() as just onSubmit() instead of onSubmit = () =>. Any ideas? And yes I have verified that my if (milestoneBtnLabel === "Create") is executing.
constructor(props) {
super(props);
this.state = {
campus: []
};
this.onSubmit = this.onSubmit.bind(this);
}
onSubmit = e => {
const { milestoneBtnLabel, schoolData } = this.props;
e.preventDefault();
if (milestoneBtnLabel === "Create") {
this.setState(
{
campus: this.state.campus.concat(schoolData.schoolData.name)
},
() => {
console.log("here"); <-- Doesn't execute
this.props.saveChecklistItem({ ...this.state });
}
);
}
this.props.closeModal();
};

Maybe this.props.closeModal(); removes the component from DOM before setState() completes. Try moving that call to setState callback.
if (milestoneBtnLabel === "Create") {
this.setState(
{
campus: this.state.campus.concat(schoolData.schoolData.name)
},
() => {
console.log("here"); <-- Doesn't execute
this.props.saveChecklistItem({ ...this.state });
this.props.closeModal();
}
);
else {
this.props.closeModal();
}

There are couple of things in your code that needs correction
No need for manual binding when the function is declared as arrow function
Never recommend mutating an array using concate instead use previous state to push new values into array
Also you need to find a better way to close the model like making it wait for 2 seconds and then close the model. Because JavaScript execution will be in parellel so you need to make your dialog wait for previous actions to be completed. You need to think about closing the modal always whenever submit button is triggered but not just when it is only milestoneBtnLabel == "Create"
Change
onSubmit = e => {
const { milestoneBtnLabel, schoolData } = this.props;
e.preventDefault();
if (milestoneBtnLabel === "Create") {
this.setState(
{
campus: this.state.campus.concat(schoolData.schoolData.name)
},
() => {
console.log("here"); <-- Doesn't execute
this.props.saveChecklistItem({ ...this.state });
}
);
}
this.props.closeModal();
}
To
onSubmit = e => {
const { milestoneBtnLabel, schoolData, closeModal, saveChecklistItem} = this.props;
e.preventDefault();
if (milestoneBtnLabel === "Create" && schoolData && schoolData.schoolData){
this.setState( prevState => (
{
campus: [...prevState.campus, schoolData.schoolData.name]
}),
() => {
console.log("here"); <-- Doesn't execute
saveChecklistItem({ ...this.state });
}
);
}
setTimeout(()=>{
closeModal();
}, 2000);
}

Related

Limiting addEventListener to componentDidMount using useEffect hook

I have a class-based component which uses multitouch to add child nodes to an svg and this works well. Now, I am trying to update it to use a functional component with hooks, if for no other reason than to better understand them.
In order to stop the browser using the touch events for gestures, I need to preventDefault on them which requires them to not be passive and, because of the lack of exposure of the passive configuration within synthetic react events I've needed to use svgRef.current.addEventListener('touchstart', handler, {passive: false}). I do this in the componentDidMount() lifecycle hook and clear it in the componentWillUnmount() hook within the class.
When I translate this to a functional component with hooks, I end up with the following:
export default function Board(props) {
const [touchPoints, setTouchPoints] = useState([]);
const svg = useRef();
useEffect(() => {
console.log('add touch start');
svg.current.addEventListener('touchstart', handleTouchStart, { passive: false });
return () => {
console.log('remove touch start');
svg.current.removeEventListener('touchstart', handleTouchStart, { passive: false });
}
});
useEffect(() => {
console.log('add touch move');
svg.current.addEventListener('touchmove', handleTouchMove, { passive: false });
return () => {
console.log('remove touch move');
svg.current.removeEventListener('touchmove', handleTouchMove, { passive: false });
}
});
useEffect(() => {
console.log('add touch end');
svg.current.addEventListener('touchcancel', handleTouchEnd, { passive: false });
svg.current.addEventListener('touchend', handleTouchEnd, { passive: false });
return () => {
console.log('remove touch end');
svg.current.removeEventListener('touchend', handleTouchEnd, { passive: false });
svg.current.removeEventListener('touchcancel', handleTouchEnd, { passive: false });
}
});
const handleTouchStart = useCallback((e) => {
e.preventDefault();
// copy the state, mutate it, re-apply it
const tp = touchPoints.slice();
// note e.changedTouches is a TouchList not an array
// so we can't map over it
for (var i = 0; i < e.changedTouches.length; i++) {
const touch = e.changedTouches[i];
tp.push(touch);
}
setTouchPoints(tp);
}, [touchPoints, setTouchPoints]);
const handleTouchMove = useCallback((e) => {
e.preventDefault();
const tp = touchPoints.slice();
for (var i = 0; i < e.changedTouches.length; i++) {
const touch = e.changedTouches[i];
// call helper function to get the Id of the touch
const index = getTouchIndexById(tp, touch);
if (index < 0) continue;
tp[index] = touch;
}
setTouchPoints(tp);
}, [touchPoints, setTouchPoints]);
const handleTouchEnd = useCallback((e) => {
e.preventDefault();
const tp = touchPoints.slice();
for (var i = 0; i < e.changedTouches.length; i++) {
const touch = e.changedTouches[i];
const index = getTouchIndexById(tp, touch);
tp.splice(index, 1);
}
setTouchPoints(tp);
}, [touchPoints, setTouchPoints]);
return (
<svg
xmlns={ vars.SVG_NS }
width={ window.innerWidth }
height={ window.innerHeight }
>
{
touchPoints.map(touchpoint =>
<TouchCircle
ref={ svg }
key={ touchpoint.identifier }
cx={ touchpoint.pageX }
cy={ touchpoint.pageY }
colour={ generateColour() }
/>
)
}
</svg>
);
}
The issue this raises is that every time there is a render update, the event listeners all get removed and re-added. This causes the handleTouchEnd to be removed before it has a chance to clear up added touches among other oddities. I'm also finding that the touch events aren't working unless i use a gesture to get out of the browser which triggers an update, removing the existing listeners and adding a fresh set.
I've attempted to use the dependency list in useEffect and I have seen several people referencing useCallback and useRef but I haven't been able to make this work any better (ie, the logs for removing and then re-adding the event listeners still all fire on every update).
Is there a way to make the useEffect only fire once on mount and then clean up on unmount or should i abandon hooks for this component and stick with the class based one which is working well?
Edit
I've also tried moving each event listener into its own useEffect and get the following console logs:
remove touch start
remove touch move
remove touch end
add touch start
add touch move
add touch end
Edit 2
A couple of people have suggested adding a dependency array which I've tried like this:
useEffect(() => {
console.log('add touch start');
svg.current.addEventListener('touchstart', handleTouchStart, { passive: false });
return () => {
console.log('remove touch start');
svg.current.removeEventListener('touchstart', handleTouchStart, { passive: false });
}
}, [handleTouchStart]);
useEffect(() => {
console.log('add touch move');
svg.current.addEventListener('touchmove', handleTouchMove, { passive: false });
return () => {
console.log('remove touch move');
svg.current.removeEventListener('touchmove', handleTouchMove, { passive: false });
}
}, [handleTouchMove]);
useEffect(() => {
console.log('add touch end');
svg.current.addEventListener('touchcancel', handleTouchEnd, { passive: false });
svg.current.addEventListener('touchend', handleTouchEnd, { passive: false });
return () => {
console.log('remove touch end');
svg.current.removeEventListener('touchend', handleTouchEnd, { passive: false });
svg.current.removeEventListener('touchcancel', handleTouchEnd, { passive: false });
}
}, [handleTouchEnd]);
but I'm still receiving a log to say that each of the useEffects have been removed and then re-added on each update (so every touchstart, touchmove or touchend which causes a paint - which is a lot :) )
Edit 3
I've replaced window.(add/remove)EventListener with useRef()
ta
If you only want this to happen when the component is mounted and unmounted, you will need to supply the useEffect hook with an empty array as the dependency array.
useEffect(() => {
console.log('adding event listeners');
window.addEventListener('touchstart', handleTouchStart, { passive: false });
window.addEventListener('touchend', handleTouchEnd, { passive: false });
window.addEventListener('touchcancel', handleTouchEnd, { passive: false });
window.addEventListener('touchmove', handleTouchMove, { passive: false });
return () => {
console.log('removing event listeners');
window.removeEventListener('touchstart', handleTouchStart, { passive: false });
window.removeEventListener('touchend', handleTouchEnd, { passive: false });
window.removeEventListener('touchcancel', handleTouchEnd, { passive: false });
window.removeEventListener('touchmove', handleTouchMove, { passive: false });
}
}, []);
Thanks a lot guys - we got to the bottom of it (w00t)
In order to stop the component useEffect hook firing multiple times, it is required to supply an empty dependency array to the hook (as suggested by Son Nguyen and wentjun) however this meant that the current touchPoints state was not accessible within the handlers.
The answer (suggested by wentjun) was in How to fix missing dependency warning when using useEffect React Hook?
which mentions the hooks faq: https://reactjs.org/docs/hooks-faq.html#what-can-i-do-if-my-effect-dependencies-change-too-often
this is how my component ended up
export default function Board(props) {
const [touchPoints, setTouchPoints] = useState([]);
const svg = useRef();
useEffect(() => {
// required for the return value
const svgRef = svg.current;
const handleTouchStart = (e) => {
e.preventDefault();
// use functional version of mutator
setTouchPoints(tp => {
// duplicate array
tp = tp.slice();
// note e.changedTouches is a TouchList not an array
// so we can't map over it
for (var i = 0; i < e.changedTouches.length; i++) {
const touch = e.changedTouches[i];
const angle = getAngleFromCenter(touch.pageX, touch.pageY);
tp.push({ touch, angle });
}
return tp;
});
};
const handleTouchMove = (e) => {
e.preventDefault();
setTouchPoints(tp => {
tp = tp.slice();
// move existing TouchCircle with same key
for (var i = 0; i < e.changedTouches.length; i++) {
const touch = e.changedTouches[i];
const index = getTouchIndexById(tp, touch);
if (index < 0) continue;
tp[index].touch = touch;
tp[index].angle = getAngleFromCenter(touch.pageX, touch.pageY);
}
return tp;
});
};
const handleTouchEnd = (e) => {
e.preventDefault();
setTouchPoints(tp => {
tp = tp.slice();
// delete existing TouchCircle with same key
for (var i = 0; i < e.changedTouches.length; i++) {
const touch = e.changedTouches[i];
const index = getTouchIndexById(tp, touch);
if (index < 0) continue;
tp.splice(index, 1);
}
return tp;
});
};
console.log('add touch listeners'); // only fires once
svgRef.addEventListener('touchstart', handleTouchStart, { passive: false });
svgRef.addEventListener('touchmove', handleTouchMove, { passive: false });
svgRef.addEventListener('touchcancel', handleTouchEnd, { passive: false });
svgRef.addEventListener('touchend', handleTouchEnd, { passive: false });
return () => {
console.log('remove touch listeners');
svgRef.removeEventListener('touchstart', handleTouchStart, { passive: false });
svgRef.removeEventListener('touchmove', handleTouchMove, { passive: false });
svgRef.removeEventListener('touchend', handleTouchEnd, { passive: false });
svgRef.removeEventListener('touchcancel', handleTouchEnd, { passive: false });
}
}, [setTouchPoints]);
return (
<svg
ref={ svg }
xmlns={ vars.SVG_NS }
width={ window.innerWidth }
height={ window.innerHeight }
>
{
touchPoints.map(touchpoint =>
<TouchCircle
key={ touchpoint.touch.identifier }
cx={ touchpoint.touch.pageX }
cy={ touchpoint.touch.pageY }
colour={ generateColour() }
/>
)
}
</svg>
);
}
Note: I added setTouchPoints to the dependency list to be more declarative
Mondo respect guys
;oB

How to refactor componentWillReceiveProps to componentDidUpdate or possibly a Hook?

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.

Render method doesnt display the state value in React Native

I am setting state inside a function which I call in componentDidMount(), but I am not accessing the value of state in the render.
How to access the state inside the render method on time?
state:
constructor() {
super();
this.state = {
check_for_amount: '',
};
}
componentdidmount() :
componentDidMount() {
this.check_amount_left();
}
function:
check_amount_left = () => {
const getSelected = this.props.navigation.state.params;
var ref = firebase.firestore().collection('discounts').where("rest_id", "==", getSelected.rest_id)
ref.onSnapshot((querySnapshot => {
var amount = querySnapshot.docs.map(doc => doc.data().amount);
this.setState({
check_for_amount: amount
});
}));
}
Render:
render() {
return(
<View/>
<Text>
{this.state.check_for_amount}
</Text>
</View>
)
}
You got wrong () at onSnapshot. Please check below to see it works for you. If not, try to log inside onSnapshot to see if it called properly.
check_amount_left = () => {
const getSelected = this.props.navigation.state.params;
var ref = firebase.firestore().collection('discounts').where("rest_id", "==", getSelected.rest_id)
ref.onSnapshot(querySnapshot => {
var amount = querySnapshot.docs.map(doc => doc.data().amount);
this.setState({
check_for_amount: amount
});
});
}

how to handle race conditions in class components?

Suppose there is a component where ask server to do some search and response will be rendered. How to ensure most recent request's response is rendered even if server side for any reason answers in different ordering? I'm not asking about cancelling previous request since it's not always possible with reasonable efforts.
onClick = () => {
apiCall(this.state.searchQuery).then(items => this.setState({ items }));
};
Is there elegant way to handle that? By now I know few approaches:
disabling button till request comes(provides bad experiences in large amount of cases - say for searching while typing)
checking inside then() if request's params matches this.props/this.state data(does not handle case when we intentionally forced new search with same query - say by pressing Enter/clicking "Search" button)
onClick = () => {
const searchQuery = this.state.searchQuery;
apiCall(searchQuery)
.then(items =>
this.state.searchQuery === searchQuery
&& this.setState({ items })
);
};
marking requests somehow and checking if it's latest(works, but looks too verboose especially if there are few requests we need to check)
searchQueryIndex = 0;
onClick = () => {
this.searchQueryIndex++;
const index = this.searchQueryIndex;
apiCall(this.state.searchQuery)
.then(items =>
this.searchQueryIndex === searchQueryIndex
&& this.setState({ items })
);
};
I'd call that trio "ugly, broken and messy".
Is there something such clear way as hooks allow:
useEffect(() => {
const isCanceled = false;
apiCall(searchQuery).then(items => !isCanceled && setItems(items));
return () => {isCanceled = true;};
}, [searchQuery])
Your onClick handler suggest a class component since you use this and this.setState:
onClick = () => {
apiCall(this.state.searchQuery).then(items =>
this.setState({ items })
);
};
I adjusted onlyLastRequestedPromise to take a function that will return something (you can return Promise.reject('cancelled') or anything).
const onlyLastRequestedPromise = (promiseIds => {
const whenResolve = (
promise,
id,
promiseID,
resolveValue,
whenCancelled = () => Promise.reject('cancelled')
) => {
if (promise !== undefined) {
//called by user adding a promise
promiseIds[id] = {};
} else {
//called because promise is resolved
return promiseID === promiseIds[id]
? Promise.resolve(resolveValue)
: whenCancelled(resolveValue);
}
return (function(currentPromiseID) {
return promise.then(function(result) {
return whenResolve(
undefined,
id,
currentPromiseID,
result
);
});
})(promiseIds[id]);
};
return (id = 'general', whenCancelled) => promise =>
whenResolve(
promise,
id,
undefined,
undefined,
whenCancelled
);
})({});
A class example on how to use it:
class Component extends React.Component {
CANCELLED = {};
last = onlyLastRequestedPromise(
'search',
() => this.CANCELLED
);
onSearch = () => {
this.last(apiCall(this.state.searchQuery)).then(
items =>
items !== this.CANCELLED && this.setState({ items })
);
};
changeAndSearch = e => {
this.setState(
{}, //state with new value
() => this.onSearch() //onSearch after state update
);
};
render() {
return (
<div>
<SearchButton onClick={this.onSearch} />
<Other onChange={this.changeAndSearch} />
</div>
);
}
}
I agree it's a lot of code but since you put most of the implementation in the lib it should not clutter your components.
If you had a functional component you could create the last function with useRef:
//
function ComponentContainer(props) {
const CANCELLED = useRef({});
const last = useRef(
onlyLastRequestedPromise('search', () => CANCELLED)
);
const [searchQuery,setSearchQuery] = useState({});
const mounted = useIsMounted();
const onSearch = useCallback(
last(apiCall(searchQuery)).then(
items =>
items !== CANCELLED &&
mounted.current &&
//do something with items
)
);
}
Finally figured out how to utilize closure to mimic "just ignore that" approach from hooks' world:
class MyComponent extends React.Component {
const ignorePrevRequest = () => {}; // empty function by default
loadSomeData() {
this.ignorePrevRequest();
let cancelled = false;
this.ignorePrevRequest = () => { cancelled = true; }; // closure comes to play
doSomeCall().then(data => !cancelled && this.setState({ data }))
}
}

Cannot access state inside callback

I've foreach function where I'm iterating the markers and calling getDetails function. At the end of getDetails, I'm calling callback function when all elements are processed. Inside callback I'm trying to access state property but I'm unable to access it. I've tried using bind method however error I'm getting is Uncaught TypeError: Cannot read property 'callback' of null.
export default class extends Component {
constructor(props) {
super(props);
this.state = {
markers: []
value: ''
}
handleChange(event) {
callback = () => {
console.log(this.state.markers);
}
markers.forEach((place,index) => {
.....
services.getDetails(request, (place, status) => {
if(place != null){
.....
}else{
}
itemsProcessed++;
if(itemsProcessed === markers.length) {
this.callback();
}
});
});
}
}
Declare callback function with arrow function like this
callback = () => {
console.log(this.state.markers);
}
And call it like this:
if(itemsProcessed === markers.length) {
this.callback();
}
Update::
You are not binding your handlechange function with class. So first you need to bind your handlechange function. A better way to do this is to use arrow function like this::
handleChange = (event) => {
Now if you want to keep your callback function out of handlechange function then you can do like this (Link of working example)::
callback = () => {
console.log(this.state.markers);
}
handleChange = (event) => {
...
markers.forEach((place,index) => {
services.getDetails(place, (place, status) => {
itemsProcessed++;
if(itemsProcessed === markers.length) {
this.callback();
}
});
});
}
Or if you want to keep your callback function inside handlechang function then you can do it like this(Link of working example)::
handleChange = (event) => {
let callback = () => {
console.log(this.state.markers);
}
...
markers.forEach((place,index) => {
services.getDetails(place, (place, status) => {
itemsProcessed++;
if(itemsProcessed === markers.length) {
callback();
}
});
});
}

Resources