How to refactor componentWillReceiveProps to componentDidUpdate or possibly a Hook? - reactjs

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.

Related

react useCallback not updating function

Isn't the hook useCallback supposed to return an updated function every time a dependency change?
I wrote this code sandbox trying to reduce the problem I'm facing in my real app to the minimum reproducible example.
import { useCallback, useState } from "react";
const fields = [
{
name: "first_name",
onSubmitTransformer: (x) => "",
defaultValue: ""
},
{
name: "last_name",
onSubmitTransformer: (x) => x.replace("0", ""),
defaultValue: ""
}
];
export default function App() {
const [instance, setInstance] = useState(
fields.reduce(
(acc, { name, defaultValue }) => ({ ...acc, [name]: defaultValue }),
{}
)
);
const onChange = (name, e) =>
setInstance((instance) => ({ ...instance, [name]: e.target.value }));
const validate = useCallback(() => {
Object.entries(instance).forEach(([k, v]) => {
if (v === "") {
console.log("error while validating", k, "value cannot be empty");
}
});
}, [instance]);
const onSubmit = useCallback(
(e) => {
e.preventDefault();
e.stopPropagation();
setInstance((instance) =>
fields.reduce(
(acc, { name, onSubmitTransformer }) => ({
...acc,
[name]: onSubmitTransformer(acc[name])
}),
instance
)
);
validate();
},
[validate]
);
return (
<div className="App">
<form onSubmit={onSubmit}>
{fields.map(({ name }) => (
<input
key={`field_${name}`}
placeholder={name}
value={instance[name]}
onChange={(e) => onChange(name, e)}
/>
))}
<button type="submit">Create object</button>
</form>
</div>
);
}
This is my code. Basically it renders a form based on fields. Fields is a list of objects containing characteristics of the field. Among characteristic there one called onSubmitTransformer that is applied when user submit the form. When user submit the form after tranforming values, a validation is performed. I wrapped validate inside a useCallback hook because it uses instance value that is changed right before by transform function.
To test the code sandbox example please type something is first_name input field and submit.
Expected behaviour would be to see in the console the error log statement for first_name as transformer is going to change it to ''.
Problem is validate seems to not update properly.
This seems like an issue with understanding how React lifecycle works. Calling setInstance will not update instance immediately, instead instance will be updated on the next render. Similarly, validate will not update until the next render. So within your onSubmit function, you trigger a rerender by calling setInstance, but then run validate using the value of instance at the beginning of this render (before the onSubmitTransformer functions have run).
A simple way to fix this is to refactor validate so that it accepts a value for instance instead of using the one from state directly. Then transform the values on instance outside of setInstance.
Here's an example:
function App() {
// setup
const validate = useCallback((instance) => {
// validate as usual
}, []);
const onSubmit = useCallback((e) => {
e.preventDefault();
e.stopPropagation();
const transformedInstance = fields.reduce((acc, {name, onSubmitTransformer}) => ({
...acc,
[name]: onSubmitTransformer(acc[name]),
}), instance);
setInstance(transformedInstance);
validate(transformedInstance);
}, [instance, validate]);
// rest of component
}
Now the only worry might be using a stale version of instance (which could happen if instance is updated and onSubmit is called in the same render). If you're concerned about this, you could add a ref value for instance and use that for submission and validation. This way would be a bit closer to your current code.
Here's an alternate example using that approach:
function App() {
const [instance, setInstance] = useState(/* ... */);
const instanceRef = useRef(instance);
useEffect(() => {
instanceRef.current = instance;
}, [instance]);
const validate = useCallback(() => {
Object.entries(instanceRef.current).forEach(([k, v]) => {
if (v === "") {
console.log("error while validating", k, "value cannot be empty");
}
});
}, []);
const onSubmit = useCallback((e) => {
e.preventDefault();
e.stopPropagation();
const transformedInstance = fields.reduce((acc, {name, onSubmitTransformer}) => ({
...acc,
[name]: onSubmitTransformer(acc[name]),
}), instanceRef.current);
setInstance(transformedInstance);
validate(transformedInstance);
}, [validate]);
// rest of component
}

How can split nested setStates when the children needs the parent previous state?

I have a warning on this nested setState pointing a "bad setState call" because I am calling a setState inside a setState, so I understand I must avoid that and take out the children setState from there, the problem is that the children is using the parent prevState for a conditional so I don't know how to solve this.
This is my code:
const setStateImageAndIndex = (state, image) => {
setSketchState(prevState => {
if (state.index !== undefined && prevState.index !== state.index) {
setCurrentSketchIndex(state.index);
}
const new_state = {
...prevState,
...state,
image: image
};
return new_state;
});
};
The warning is pointing the setCurrentSketchIndex(state.index); explicitly, which is the function I want to take out from setSketchState. Thank you!
prevState in setSketchState infact is the actual state of sketchState (I mean the state setted by setSketchState). So you could write something like:
const setStateImageAndIndex = (state, image) => {
if (state.index !== undefined && sketchState.index !== state.index) {
setCurrentSketchIndex(state.index);
}
setSketchState(prevState => {
const new_state = {
...prevState,
...state,
image: image
};
return new_state;
});
};
you can hold the previous state of the parent component in a useRef and derivate the state in the child component passing that value down as a prop, for example:
function Parent() {
let [state, setState] = React.useState({ version: 1 });
let prevState = usePrevious(state.version);
return (
<div>
<p>Parent value: {state.version}</p>
<Child value={prevState} />
<button
onClick={() =>
setState({
version: state.version + 1
})
}
>
Update version
</button>
</div>
);
}
function Child(props) {
return (
<div>
<p>Child value: {props.value || "No value yet"}</p>
</div>
);
}
as we can see above, the parent component will update the state.version in the button click and usePrevious will hold the previous value:
function usePrevious(value) {
const ref = React.useRef();
React.useEffect(() => {
ref.current = value;
});
return ref.current;
}
Working example in: https://codesandbox.io/s/use-previous-hook-0iemv
According to the comments above from #GabrielePetrioli and #GiovanniEsposito, this solution seemed to be safer.
const setStateImageAndIndex = (state, image) => {
let updateCurrentSketchIndex = false;
setSketchState(prevState => {
if (state.index !== undefined && prevState.index !== state.index) {
updateCurrentSketchIndex = true;
}
const new_state = {
...prevState,
...state,
image: image
};
return new_state;
});
if (updateCurrentSketchIndex) {
setCurrentSketchIndex(state.index);
}
};

React: Cannot show button when element is true

this is my code:
const Agregar = ({inputPregunta, validatePregunta}) => {
return(
<div id="content">
<h2>¿Cual es tu pregunta?</h2>
<input id="valorPregunta" type="text" placeholder="Añade aqui tu pregunta..." onChange={(e) => inputPregunta(e)}/>
{validatePregunta && <button>Agregar</button>}
</div>
);
}
What i am trying to do is when the input has something entered the prop validatePregunta (default is false) comes to true and the button element shows, for that i tried to do a method in the App.js file like this:
actualizarPregunta = (e) => {
this.setState({inputPregunta: e.target.value})
if(this.state.inputPregunta.trim().length > 0){
this.setState({validatePregunta: true})
} else {
this.setState({validatePregunta: false})
}
}
But nothing shows, is there's something that i am doing wrong?
Edit: Here is the code of the rendering for the props:
renderRoute = () => {
switch(this.state.route) {
case 'menu':
return (
<div>
<Header />
<Agregar inputPregunta={this.actualizarPregunta} validate={this.state.validatePregunta}/>
<Publications />
</div>
)
default :
return (
<div>
<Header />
<Publications />
</div>
)
}
}
render() {
return (
<div>
{
this.renderRoute(this.state.route)
}
</div>
);
}
This is how you use the compoennt
<Agregar inputPregunta={this.actualizarPregunta} validate={this.state.validatePregunta}/>
you are passing the value of this.state.validatePregunta by property name of validate
and then you are expecting something validatePregunta in component Agregar, but it should be actually validate
const Agregar = ({inputPregunta, validatePregunta}) => { //incorrect
const Agregar = ({inputPregunta, validate}) => { //correct
OR else simply change the prop name as below
<Agregar inputPregunta={this.actualizarPregunta} validatePregunta={this.state.validatePregunta}/>
And you should change actualizarPregunta once you update the state it's not updating the state value real time, its a async process, so this.state.inputPregunta.trim() will give you the value just before the update, so change it like this, and i love the way #Drew Reese handle this part
actualizarPregunta = (e) => {
const newValue = e.target.value;
this.setState({inputPregunta: newValue })
if(newValue.trim().length > 0){
this.setState({validatePregunta: true})
} else {
this.setState({validatePregunta: false})
}
}
Issue 1
The props don't align.
The component signature is const Agregar = ({inputPregunta, validatePregunta}) but you pass validate={this.state.validatePregunta}.
<Agregar
inputPregunta={this.actualizarPregunta}
validate={this.state.validatePregunta}
/>
Solution
Align on prop name usage.
<Agregar
inputPregunta={this.actualizarPregunta}
validatePregunta={this.state.validatePregunta}
/>
Issue 2
React component state lifecycle. The enqueued state is attempted to be referenced within the same react cycle that enqueue it. You need the enqueued value which won't be available until the next render cycle.
actualizarPregunta = (e) => {
this.setState({ inputPregunta: e.target.value }); // <-- state update enqueued
if (this.state.inputPregunta.trim().length > 0){ // <-- current state value
this.setState({ validatePregunta: true })
} else {
this.setState({ validatePregunta: false })
}
}
Solution 1
Enqueue the state update and use the current event value to set the other state
actualizarPregunta = (e) => {
const { value } = e.target;
this.setState({
inputPregunta: value,
validatePregunta: !!value.trim().length, // <-- *
});
}
* length == 0 is falsey, length !== 0 is truthy, and coerced to boolean (!!)
Solution 2
Enqueue the state update and use componentDidUpdate lifecycle function to handle effect
actualizarPregunta = (e) => {
const { value } = e.target;
this.setState({ inputPregunta: value });
}
componentDidUpdate(prevProps, prevState) {
const { inputPregunta } = this.state;
if (prevState.inputPregunta !== inputPregunta) {
this.setState({ validatePregunta: !!inputPregunta.trim().length });
}
}
Solution #1 is the simpler and more straight forward.

Having trouble making a prop update dynamically in countdown timer - create with a react hook

I'm new to React and building a project that requires dynamically setting a countdown timer from a parent component. I found a react countdown timer online that uses a hook, but I'm not too familiar hooks yet.
Toward the bottom of my code, you can see a parent class where I'm passing 'cycleTimeSelected' to the Countdown component/hook. It works correctly up to that point.
But I'm not having success getting it to update the timer correctly and dynamically. timeRemaining in React.useState() is the variable I need to update. It doesn't work if I use props.cycleTimeSelected directly. I think I understand why that is, so I tried to use componentWillRecieveProps to set the state, but that's not working.
I'm think I'm confused about React.useState and how that relates to setState for one thing. Can anyone spot my problem here?
import React, { Component } from 'react'
import { PausePresentation, SkipNext, Stop, PlayCircleOutline} from '#material-ui/icons';
import './Timer.css'
function Countdown(props) {
const [timer, setTimer] = React.useState({
name: 'timer',
isPaused: true,
time: 100,
timeRemaining: props.cycleTimeSelected,
timerHandler: null
})
//const componentWillReceiveProps = nextProps => {
//this.setState({ timeRemaining: nextProps.cycleTimeSelected });
//}
const handleTimeChange = e => {
setTimer({
...timer,
time: props.cycleTimeSelected,
timeRemaining: Number(e.target.value),
})
}
React.useEffect(() => {
if (timer.timeRemaining === 0) {
clearInterval(timer.timerHandler)
}
}, [timer.timeRemaining, timer.timerHandler])
const updateTimeRemaining = e => {
setTimer(prev => {
return { ...prev, timeRemaining: prev.timeRemaining - 1 }
})
}
const handleStart = e => {
const handle = setInterval(updateTimeRemaining, 1000);
setTimer({ ...timer, isPaused: false, timerHandler: handle })
}
const handlePause = e => {
clearInterval(timer.timerHandler)
setTimer({ ...timer, isPaused: true })
}
return <React.Fragment>
{/* { <input value={props.cycleTimeSelected} type="" onChange={handleTimeChange} /> } */}
{timer.name && timer.time && <div className={timer.timeRemaining === 0 ? 'time-out':''}>
{timer.isPaused && <div className="floatLeft"><i className="material-icons pause"><PlayCircleOutline onClick={handleStart}></PlayCircleOutline></i></div>}
{!timer.isPaused && <div className="floatLeft"><i className="material-icons pause"><PausePresentation onClick={handlePause}></PausePresentation></i></div>}
{`Remaining: ${timer.timeRemaining}`}
</div>}
</React.Fragment>
}
class Timer extends React.Component {
render(){
return(
<>
<div className="floatLeft"><i className="material-icons bottom-toolbar stop"><Stop onClick={this.clickStop}></Stop></i></div>
<div className="floatLeft"><i className="material-icons bottom-toolbar skip_next"><SkipNext onClick={this.clickSkip}></SkipNext></i></div>
<div className="floatLeft"><div id="timer"><Countdown cycleTimeSelected={this.props.cycleTimeSelected}></Countdown></div></div>
</>
);
}
}
If you want timer.timeRemaining in Countdown to update via props then you should implement an effect with a dependency on props.cycleTimeSelected. useEffect is nearly the functional component equivalent to a class-based component's componentDidMount, componentDidUpdate, and componentWillUnmount lifecycle functions. We're interested in the "update" lifecycle.
useEffect(() => {
setTimer((timer) => ({
...timer, // copy existing state
timeRemaining: props.cycleTimeSelected // update property
}));
}, [props.cycleTimeSelected]);

Why is setState is not working as expected

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

Resources