React useEffect has missing dependencies - reactjs

Hi there i'm new to react i'm setting state and city using pincode when it has length of 6 digits when i put whole function inside useEffect it gives error to include setPersonalDetState & i also want to use same funtion to validate i cannot include it inside useEffect
const intialState = {
city: '',
state: '',
pincode: ''
};
const PersonalDetails = () => {
const [personalDetState, setPersonalDetState] = useState(intialState);
const { city, state, pincode } = personalDetState;
const fetchPincode = async (pincode) => {
if (pincode.length != 6) {
return;
}
let cityState = await axios.get(
`https://api.postalpincode.in/pincode/${pincode}`
);
const { Status, PostOffice } = cityState.data[0];
const { District, State } = PostOffice[0];
personalDetState['city'] = District;
personalDetState['state'] = State;
return setPersonalDetState({ ...personalDetState });
};
useEffect(() => {
fetchPincode(pincode);
}, [pincode]);
const handleChange = (event) => {
let { value, name } = event.target;
if (name === 'pincode') value = value.replace(/\D/g, '');
if (name === 'pincode' && value.length === 7) return;
setPersonalDetState({ ...personalDetState, [name]: value });
};
return (
<Fragment>
<input
type='text'
name='pincode'
value={pincode}
onChange={handleChange}
/>
<input type='text' name='city' value={city} disabled />
<input type='text' name='state' value={state} disabled />
</Fragment>
);
};

You can useCallback for fetchPincode in order to correctly specify it as a dependency to the effect. It will run whenever pincode changes, but since fetchPincode callback doesn't have dependencies, it won't trigger the effect.
const intialState = {
city: '',
state: '',
pincode: ''
};
const PersonalDetails = () => {
const [personalDetState, setPersonalDetState] = useState(intialState);
const { city, state, pincode } = personalDetState;
const fetchPincode = useCallback(() => {
if (pincode.length != 6) {
return;
}
axios.get(
`https://api.postalpincode.in/pincode/${pincode}`
).then(cityState => {
const { Status, PostOffice } = cityState.data[0];
const { District, State } = PostOffice[0];
setPersonalDetState(prev => ({
city: District,
state: State
}));
});
}, [pincode]);
useEffect(fetchPincode, [pincode]);
const handleChange = (event) => {
let { value, name } = event.target;
if (name === 'pincode') value = value.replace(/\D/g, '');
if (name === 'pincode' && value.length === 7) return;
setCredentials({ ...userCredentials, [name]: value });
};
return (
<Fragment>
<input
type='text'
name='pincode'
value={pincode}
onChange={handleChange}
/>
<input type='text' name='city' value={city} disabled />
<input type='text' name='state' value={state} disabled />
</Fragment>
);
};
I've rewritten the fetchPincode to use the old-fashion Promise syntax, because it must not return anything in order to be used as useEffect callback. I answered a similar question here.
Why useCallback? In order to keep the same function reference between re-renders. Reference to the function will change if its dependencies change (dependencies are specified as array right after the function, as second parameter to useCallback).
Why the same function reference? Because if included as a dependency to another hook (say useEffect), changing reference may cause the effect to re-run (in case of effects), which is probably not desired if that function reference changes too often.
In your particular case, pincode will change whenever you type (not 100% sure, but I assume setCredentials is doing that, I didn't see its declaration). This will cause the function reference to fetchPincode to change. If specified as a dependency to an effect, the effect would run if the function ref changes. But you also have pincode specified as dependency so effect would run on each key input anyway. But you might think about making the request after specified period of inactivity (user not typing), also known as debounce.
To learn about hooks:
Introducing hooks
Hooks FAQ

Related

React form useState object doesn't work as I want

I have a problem with a React app. I have a form with two inputs, and when I submit the form with empty inputs, it should render an error message in each of them. The problem is that it doesn't show for the first input. How can I fix it to display an error in each of those? The implementation is in useForm.js.
FormUI image:
My code:
Form.js
const Form = () => {
const formLogin = () => {
console.log("Callback function when form is submitted!");
console.log("Form Values ", values);
}
const {handleChange, values, errors, handleSubmit} = useForm(formLogin);
return (
<Wrapper>
<form onSubmit={handleSubmit}>
<div className="govgr-form-group gap-bottom">
<label className="govgr-label govgr-!-font-weight-bold" htmlFor="code">Code*</label>
{errors.code && <p className="govgr-error-message"><span className="govgr-visually-hidden">Λάθος:</span>{errors.code}</p>}
<input className={`govgr-input govgr-!-width-three-quarter ${errors.code ? 'govgr-error-input' : ''}`} id="code" name="code" type="text" onChange={handleChange} />
</div>
<fieldset>
<div className="govgr-form-group">
<label className="govgr-label govgr-!-font-weight-bold" htmlFor="first">Name*</label>
{errors.first && <p className="govgr-error-message"><span className="govgr-visually-hidden">Λάθος:</span>{errors.first}</p>}
<input className={`govgr-input govgr-!-width-three-quarter ${errors.first ? 'govgr-error-input' : ''}`} id="first" name="first" type="text" onChange={handleChange} />
</div>
</fieldset>
<button type="submit" className="govgr-btn govgr-btn-primary btn-center">Save</button>
</form>
</Wrapper>
);
};
export default Form;
useForm.js:
const useForm = (callback) => {
const [values, setValues] = useState({});
const [errors, setErrors] = useState({});
const validate = (event, name, value) => {
event.persist();
switch (name) {
case "code":
if (value.trim() === "" || value.trim() === null) {
setErrors({
...errors,
code: "Code is required",
});
} else {
let newObj = omit(errors, "code");
setErrors(newObj);
}
break;
case "first":
if (value.trim() === "" || value.trim() === null) {
setErrors({
...errors,
first: "Name is required",
});
} else {
let newObj = omit(errors, "first");
setErrors(newObj);
}
break;
default:
break;
}
};
const handleChange = (event) => {
event.persist();
let name = event.target.name;
let val = event.target.value;
validate(event, name, val);
setValues({
...values,
[name]: val,
});
};
const handleSubmit = (event) => {
if (event) event.preventDefault();
if (
Object.keys(errors).length === 0 &&
Object.keys(values).length !== 0 &&
values.code &&
values.first
) {
callback();
} else {
if (!values.code) {
setErrors({
...errors,
code: "Code is required.",
});
}
if (!values.first) {
setErrors({
...errors,
first: "Name is required.",
});
}
}
};
return {
values,
errors,
handleChange,
handleSubmit
};
};
export default useForm;
You can simplify your code by having only a single validation routine. You can fix the error you mention, having only a single error at a time, by using the current state as passed into setState by the framework. The construct for this is
setState(e => /* whatever you want to return relative to e */);
Ultimately, the error you're seeing is because the component, and its code, is rerendered after every change of state (but they are batched for performance reasons), so your code is only seeing an older version of the state when you access it directly. If you want the actual current state when changing it, it's always best to have the framework pass it directly to the change-state function. If you always follow this pattern, you will rarely have any problems with the react state model.
The hard problem however isn't any of this. The hard problem is the way you're removing errors. First, from a UI perspective, it's a little inconsistent to only show a field as an error when it is edited, because the form is erroneous when it's first shown. But, ignoring that, the underlying technical problem is removing a property from an object. Normally we would use arrays rather than objects when we need to do this, or we would use underscore's omit function. I see in your code you call a function called omit, but you don't have an explanation as to what that is. In your case though, this can be easily solved by never removing the property. Instead, have each as an object with a valid flag. This would allow you to remove the switch statement completely.
You could go further and roll the value in there too, making a complete single state for each field, but I won't demonstrate that here.
I haven't tested this, so there may be a typo or two lurking in here.
const useForm = (callback) => {
const [values, setValues] = useState({code: '', first: ''});
const [errors, setErrors] = useState({first: {message: "Name is required", valid: true}, code: {message: "Code is required", valid: true}});
const isEmpty = val => val.trim() === "" || val.trim() === null);
const validate = (name, val) => {
setErrors(e => ({...e, [name]: {...e[name], valid: isEmpty(val)}}))
return errors[name].valid;
};
const handleChange = (event) => {
let name = event.target.name;
let val = event.target.value;
setValues({...values, [name]: val});
validate(name, val);
};
const handleSubmit = event => {
if (event) event.preventDefault();
const valid = validate('code', values.code) && validate('first', values.first);
if ( valid ) {
callback();
}
};
return {
values,
errors,
handleChange,
handleSubmit
};
};
export default useForm;
And, you'd have to change your HTML template slightly:
{!errors.first.valid && <p className="govgr-error-message"><span className="govgr-visually-hidden">Λάθος:</span>{errors.first.message}</p>}
I realise that this new validate function is a huge departure from what you had, but based on your own code I think you can handle it. Basically, it's just using destructuring and variable property accessors. This looks like its well within your capabilities to understand, but feel free to ask more questions if any of it is confusing or doesn't actually work :)

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
}

Useeffect not update after i select new item on Select option

I changed option but useeffect not update input. Please guide me where i make mistake. First i use useEffect to setCurrency after that i use mapping for getCurrency to add it on Select option. onCitySelect i added it to setSelectedId when i change Select option. Lastly, i tried to get address with api but the problem is i need to change api/address?currency=${selectId.id}` i added selectedId.id but everytime i change option select it is not affect and update with useEffect. I tried different solution couldn't do it. How can i update useEffect eveytime option select change (selectId.id) ?
export default function Golum() {
const router = useRouter();
const dispatch = useDispatch();
const [getCurrency, setCurrency] = useState("");
const [getAddress, setAddress] = useState("");
const [selectCity, setSelectCity] = useState("");
const [selectId, setSelectId] = useState({
id: null,
name: null,
min_deposit_amount: null,
});
const [cityOptions, setCityOptions] = useState([]);
useEffect(() => {
setSelectCity({ label: "Select City", value: null });
setCityOptions({ selectableTokens });
}, []);
const onCitySelect = (e) => {
if (e == null) {
setSelectId({
...selectId,
id: null,
name: null,
min_deposit_amount: null,
});
} else {
setSelectId({
...selectId,
id: e.value.id,
name: e.value.name,
min_deposit_amount: e.value.min_deposit_amount,
});
}
setSelectCity(e);
};
const selectableTokens =
getCurrency &&
getCurrency.map((value, key) => {
return {
value: value,
label: (
<div>
<img
src={`https://central-1.amazonaws.com/assets/icons/icon-${value.id}.png`}
height={20}
className="mr-3"
alt={key}
/>
<span className="mr-3 text-uppercase">{value.id}</span>
<span className="currency-name text-uppercase">
<span>{value.name}</span>
</span>
</div>
),
};
});
useEffect(() => {
const api = new Api();
let mounted = true;
if (!localStorage.getItem("ACCESS_TOKEN")) {
router.push("/login");
}
if (mounted && localStorage.getItem("ACCESS_TOKEN")) {
api
.getRequest(
`${process.env.NEXT_PUBLIC_URL}api/currencies`
)
.then((response) => {
const data = response.data;
dispatch(setUserData({ ...data }));
setCurrency(data);
});
}
return () => (mounted = false);
}, []);
useEffect(() => {
const api = new Api();
let mounted = true;
if (!localStorage.getItem("ACCESS_TOKEN")) {
router.push("/login");
}
if (mounted && localStorage.getItem("ACCESS_TOKEN")) {
api
.getRequest(
`${process.env.NEXT_PUBLIC_URL}api/address?currency=${selectId.id}`
)
.then((response) => {
const data = response.data;
dispatch(setUserData({ ...data }));
setAddress(data.address);
})
.catch((error) => {});
}
return () => (mounted = false);
}, []);
return (
<div className="row mt-4">
<Select
isClearable
isSearchable
onChange={onCitySelect}
value={selectCity}
options={selectableTokens}
placeholder="Select Coin"
className="col-md-4 selectCurrencyDeposit"
/>
</div>
<div className="row mt-4">
<div className="col-md-4">
<Form>
<Form.Group className="mb-3" controlId="Form.ControlTextarea">
<Form.Control className="addressInput" readOnly defaultValue={getAddress || "No Address"} />
</Form.Group>
</Form>
</div>
</div>
);
}
The second parameter of the useEffect hook is the dependency array. Here you need to specify all the values that can change over time. In case one of the values change, the useEffect hook re-runs.
Since you specified an empty dependency array, the hook only runs on the initial render of the component.
If you want the useEffect hook to re-run in case the selectId.id changes, specify it in the dependency array like this:
useEffect(() => { /* API call */ }, [selectId.id]);
I think you are accessing the e object wrong. e represents the click event and you should access the value with this line
e.target.value.id
e.target.value.value

Unable to update state for a component

I have just started learning react.
I have a component which is calling the weather API and fetching data for user provided location, user input is getting updated under userLoc but somehow the state is not getting updated for finalLoc and whenever I console log it, it is showing undefined.
const Inputs = props => {
const [userLocation, setUserLocation] = React.useState('')
const [finalLocation, setFinalLocation] = React.useState('')
function fetchLocation(e) {
setUserLocation (e.target.value)
}
function fetchDetails(e) {
e.preventDefault()
let baseURL = '//api.openweathermap.org/data/2.5/weather?q='
const API_KEY = '&API_KEY'
let total = baseURL + userLocation + API_KEY
console.log(userLocation) // Outputs the input value
setFinalLocation(userLocation)
console.log(finalLocation) // Comes as blank
console.log(total);
}
return (
<div className='inputs'>
<p className="label">Enter the location to find the weather.</p>
<input type="text" className='loc-input' autoFocus placeholder='Enter a location' name="" id="location" onChange={fetchLocation} value={loc.userLoc || ""} />
<button onClick={fetchDetails}>Get Details</button>
<Outputs loc={loc.finalLoc} results={loc.result} />
</div>
)
}
const Inputs = props => {
const [finalLoc, setFinalLoc] = React.useState("");
const [userLoc, setUserLoc] = React.useState("");
const [data, setData] = React.useState([]);
function fetchLocation(e) {
userLoc( e.target.value )
}
function fetchDetails(e) {
let baseURL = '//api.openweathermap.org/data/2.5/weather?q='
let API_KEY = '&appid=API_KEY'
let total = baseURL + loc.userLoc + API_KEY
setFinalLoc( userLoc )
fetch(total)
.then(response => response.json())
.then(data => {
setData( data )
})
}
return (
<div className='inputs'>
<p className="label">Enter the location to find the weather.</p>
<input type="text" className='loc-input' autoFocus placeholder='Enter a location' name="" id="location" onChange={fetchLocation} value={ userLoc || ""} />
<button onClick={fetchDetails}>Get Details</button>
<Outputs loc={ finalLoc} results={data} />
</div>
)
}
If you update a state when using useState you don't write it as
setState({state: //data})
But
setState(//data)
The updating method in useState is similar to class component setState method but you have to update it differently.
See more about useState here.
If your state is an object you should update it as an object:
const [state, setState] = useState({
name: "John",
lastName: "Due",
});
setState({
name: "Liam",
lastName: "call",
});
BTW,
If you are updating multiple values in your state, You should update it in one function.#1
Note:
Each time you're updating your state cuase React to re-render and slow your app.
For better performance you should always try to re-render your app as less as possible.
Edit:
#1 if your state is an object you should update it in one function.
You can use the functional form. The function will receive the previous value, and return an updated value. so to update the state we have to make copy of previous value with updated one and then set the state with updated values
setLoc((loc) => ({ ...loc, finalLoc: loc.userLoc }));
same when you get result data keep copy of previous state
setLoc((loc) => ({ ...loc, result: data }));

event.currentTarget.name returning as currentTarget

I have a handleChange function in typescript that I call within another function to send the value of changes in a text field to a mobx tree. However, when I set const { name } = event.currentTarget and later log it in the function, the name variable is coming back as 'currentTarget' instead of the name attribute I assign in in my renderHexTextField function, and the value is undefined.
I render a number of different text fields by calling the renderHexTextField function, which takes in two params. The first is the value of the
If it was working as intented, the name variable would equal the 'hoverFontColor' string from my return statement, which would then be passed into handleChange as a key for the css object, and value would manipulate the mobx state tree.
Any help is appreciated!
edit** I forgot to mention that the TextField component is a MaterialUI component
SOLUTION EDIT** -- My handleChange was bound to a debounce. I had to update my onChange component attribute so event.persist() ran before this.handleChange. Thank you Praveen and Chris!
return (
this.renderHexTextField(css.hoverFontColor, 'hoverFontColor')
)
private renderHexTextField(input: string, name: string) {
// name parameter used to specify which state in handleChange function
if (name === 'fontType' || this._throwHexErr(input) === 'True') {
// If hex format is correct, render normal text field
return (
<TextField
required={true}
id="standard-required"
margin="normal"
name={name}
placeholder={input}
onChange={this.handleChange}
/>
)
} else {
// else render error text field
return (
<TextField
error={true}
id="standard-error"
margin="normal"
name={name}
placeholder={input}
onChange={this.handleChange}
/>
)
}
}
private handleChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
const { name, value } = event.currentTarget
const { store } = this.props
const currentBot = store.bots.current
if (currentBot) {
const id = currentBot._id
const css: any = toJS(currentBot.theme.css)
log('css obj >> ', css)
if (css) {
css[name] = value
log('handleChange >>> ', name, value, css)
currentBot.patchCSS(id, css)
}
} else {
log('No current bot in handleChange')
}
}
private _validateHex(hexcode: string, regex: any) {
// Regex Testing Function
log('validating hex')
return regex.test(hexcode)
}
private _throwHexErr(userInput: string) {
// Return True or Error depending on result of Regex Test
const regex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/
if (this._validateHex(userInput, regex)) {
return 'True'
} else {
return 'Error'
}
}
I have had the same trouble recently, I have used React.FormEvent<HtmlInputElement>. That gives me event.currentTarget.name from the interface. Does that help?
So just to elaborate, try changing React.ChangeEvent<HTMLInputElement> to React.FormEvent<HtmlInputElement>.
I think you need to change
const { name, value } = event.currentTarget
to
const { name, value } = event.target
or
const name = event.target.name;
const value = event.target.value;
This should work fine
private handleChange = (event: any): void => {
const name = event.target.name;
const value = event.target.value;
const { store } = this.props
const currentBot = store.bots.current
if (currentBot) {
const id = currentBot._id
const css: any = toJS(currentBot.theme.css)
log('css obj >> ', css)
if (css) {
css[name] = value
log('handleChange >>> ', name, value, css)
currentBot.patchCSS(id, css)
}
} else {
log('No current bot in handleChange')
}
}
also, do
<TextField
error={true}
id="standard-error"
margin="normal"
name={name}
placeholder={input}
onChange={(event) => this.handleChange(event)}
/>
See my solution edit above. My handleChange function was bound to a debounce, so I had to include event.persist() in the onChange attribute.
use e.currentTarget.getAttribute('name')
example:
const handleClick = (e) => {
console.log(e.currentTarget.getAttribute('name'))
}

Resources