Cypress does not save and load state properly in React component, why? - reactjs

I have following state:
const [startPaymentIn, setStartPaymentIn] = useLocalStorage<StartPaymentIn>(
'startPaymentIn', {})
It read content from localstorage. And it is rendered in return. But no value is rendered, though I see values are set.
useEffect(() => {
if (user?.partnerData) {
setStartPaymentIn({
...startPaymentIn,
['partnerData']: user!.partnerData!,
})
setStartPaymentIn({
...startPaymentIn,
['deliveryData']: user!.deliveryData!,
})
} else {
setStartPaymentIn((prevState) => ({
...prevState,
partnerData: { country: 'Hungary' },
}))
}
}, [user])
<div className={'inputContainer'}>
<p>Név</p>
<input
value={startPaymentIn?.partnerData?.name ?? ''}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setData('partnerData', 'name', event.target.value)
}}
/>
</div>

Related

Component not render the newest value?

I'm getting used to with redux. My problem is the itemList correctly render the latest value but the value of Checkbox which is from hook state not get the latest value. It should be checked for all item list but it is not. Although I console.log the values in the map func, it still get the latest values and the find func is correct.
export default function Component(props) {
const dispatch = useDispatch();
const { itemList } = useSelector((state) => state.AllCourses);
const [values, setValues] = useState({
all: true,
items: []
});
useEffect(() => {
dispatch(
someActions.getItemList(payload)
); //this will get latest itemList
}, []);
useEffect(() => {
if (itemList.length) {
const newValues = {
all: true,
items: itemList.map((item) => ({
select: true,
id: item.id,
})),
};
setValues(newValues);
}
}, [itemList]);
return (
<Box ml={4}>
{ itemList?.map((item) => {
return (
<Box key={item.id}>
<Checkbox
name={item.name}
value={values?.items?.find((itemVal) => item.id === itemVal.id)?.select}
/>
</Box>
);
})}
</Box>
);
}
`
Tried several solutions but still not correctly
It seems like you are using Material UI. For checkbox component you need to set the checked prop.
import Checkbox from '#mui/material/Checkbox';
const label = { inputProps: { 'aria-label': 'Checkbox demo' } };
export default function Checkboxes() {
return (
<div>
<Checkbox {...label} defaultChecked />
<Checkbox {...label} />
<Checkbox {...label} disabled />
<Checkbox {...label} disabled checked />
</div>
);
}
If you use html input tag with type checkbox, then again you have to set the checked attribute accordingly see below.
<label for="vehicle2"> I have a car</label><br>
<input type="checkbox" name="vehicle3" value="Boat" checked>
And lastly, you don't need a local state in your example and you can remove
const [values, setValues] = useState({
all: true,
items: []
});
and
useEffect(() => {
if (itemList.length) {
const newValues = {
all: true,
items: itemList.map((item) => ({
select: true,
id: item.id,
})),
};
setValues(newValues);
}
}, [itemList]);
and replace with
const values = {
all: true,
items: itemList && itemList.length ? itemList.map((item) => ({
select: true,
id: item.id,
})) : [],
};

"False" appearing in input after removing its text

After inserting text in input and later removing it completely with backspace "false" appears in it. It cannot be removed - after removing last letter it appears again. It appears in input which are rendered using map() method. The same problem doesn't occur in textarea which is not rendered using this method so I guess the problem lays somewhere here, but I dont have idea where.
export default function AddTodo({ active, setActive }: AddTodoProps) {
const { isDarkMode } = useContext(DarkModeContext);
const [todoDetails, setTodoDetails] = useState({
task: "",
description: "",
name: "",
deadline: "",
});
const { submitTodo, loading, status } = useAddTodoToDb(todoDetails);
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setTodoDetails((prev) => {
return {
...prev,
[e.target.id]: e.target.value || e.target.checked,
};
});
};
useEffect(() => {
if (typeof status == "string")
setTodoDetails({
task: "",
description: "",
name: "",
deadline: "",
});
}, [status]);
return (
{todoInputs.map((item) => {
return (
<StyledLabelAndInput
isDarkMode={isDarkMode}
err={status?.includes(item.id)}
key={item.id}
>
<label htmlFor={item.id}>{item.text}</label>
<input
value={todoDetails[item.id as keyof ITodoDetails]}
type={item.type}
id={item.id}
min={today}
onChange={(e) => handleChange(e)}
/>
</StyledLabelAndInput>
);
})}
<label htmlFor="description">Description (optional)</label>
<textarea
value={todoDetails.description}
id="description"
onChange={(e) =>
setTodoDetails((prev) => {
return {
...prev,
description: e.target.value,
};
})
}
/>
The false value is comming from the input's checked attribute.
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setTodoDetails((prev) => {
return {
...prev,
[e.target.id]: e.target.value || e.target.checked, /* <--- HERE */
};
});
};
You're trying to get the input's value OR checked, and since the input has no value it's returning the checked attribute, which is false by default.
Instead you'll need to check if the item type is checkbox before using the checked attribute.

Child components not updating global state

I am developing a form with controlled components, Material-UI and, react hooks. All of the form data is saved in a global state via a useState setter function.
Additionally, some fields need to be toggled on and off depending on the user's response which implies that its local and global state has to be reset when toggled off.
That said when two or more components are toggled off at the same time one of them fails to update the global form state.
Here is my code:
App.js
imports...
function App () {
const [formState, setFormState] = useState({
fullName: '',
email: '',
ageLevel: '',
numTitle: '',
titleTypes: '',
materialType: '',
subjInterest: ''
})
const handleTxtFldChange = (e, name) => {
setFormState({ ...formState, [name]: e.target.value })
}
return (
<>
<div className='App'>
<form noValidate autoComplete='off'>
<TextField
required
value={formState.fullName}
onChange={e => handleTxtFldChange(e, 'fullName')}
label='fullName:'
/>
<AgeLevelSelect
formState={formState}
setFormState={setFormState}
/>
<NumOfTitles
formState={formState}
setFormState={setFormState}
ageLevel={formState.ageLevel}
/>
<MaterialType
formState={formState}
setFormState={setFormState}
ageLevel={formState.ageLevel}
/>
<SubjOfInterest
formState={formState}
setFormState={setFormState}
ageLevel={formState.ageLevel}
materialType={formState.materialType}
/>
<Button
onClick={() => { submitForm() }}
>
Submit
</Button>
</form>
</div>
</>
)
}
export default App
When Adult is selected from AgeLevelSelect, numTitle and materialType will be toggled on.
The data is saved in its local and global sate.
Component: AgeLevelSelect.js
imports...
const AgeLevelSelect = ({ formState, setFormState }) => {
const [age, setAge] = useState('')
const handleChange = (event) => {
setAge(event.target.value)
setFormState({ ...formState, ageLevel: event.target.value })
}
return (
<FormControl>
<InputLabel>Age level?</InputLabel>
<Select
value={age}
onChange={handleChange}
>
<MenuItem value='School-Age'>School-Age</MenuItem>
<MenuItem value='Teens'>Teens</MenuItem>
<MenuItem value='Adults'>Adults</MenuItem>
</Select>
</FormControl>
)
}
export default AgeLevelSelect
Here we select two from the select options. The data is saved in its local and global sate.
Component: NumOfTitles.js
imports...
const NumTitles = ({ formState, setFormState, ageLevel }) => {
const [titles, setTitles] = useState('')
const [isVisible, setIsVisible] = useState('')
const handleChange = (event) => {
setTitles(event.target.value)
setFormState({ ...formState, numTitle: event.target.value })
}
useEffect(() => {
if (ageLevel === 'Adults') {
setIsVisible(true)
} else {
setValue('')
setIsVisible(false)
setFormState(prevState => {
return { ...formState, materialType: '' }
})
}
}, [ageLevel])
useEffect(() => {
if (ageLevel !== 'Adults') {
setFormState(prevState => {
return { ...formState, materialType: '' }
})
setValue('')
setIsVisible(false)
}
}, [value])
return (
isVisible &&
<FormControl>
<InputLabel id='demo-simple-select-label'>Number of titles:</InputLabel>
<Select
value={titles}
onChange={handleChange}
>
<MenuItem value='One'>One</MenuItem>
<MenuItem value='Two'>Two</MenuItem>
</Select>
</FormControl>
)
}
export default NumTitles
If you made it this far THANK YOU. We are almost done.
Here we select Non-fiction. Data gets save in local and global state.
Additionally, the subject of interest question is toggled on.
Component: MaterialType.js
imports...
const TypeOfMaterial = ({ formState, setFormState, ageLevel }) => {
const [value, setValue] = useState('')
const [isVisible, setIsVisible] = useState('')
const handleChange = (event) => {
setValue(event.target.value)
setFormState({ ...formState, materialType: event.target.value })
}
useEffect(() => {
if (ageLevel === 'Adults') {
setIsVisible(true)
} else {
setValue('')
setIsVisible(false)
setFormState(prevState => {
return { ...formState, materialType: '' }
})
}
}, [ageLevel])
useEffect(() => {
if (!isVisible) {
setFormState(prevState => {
return { ...formState, materialType: '' }
})
setValue('')
setIsVisible(false)
}
}, [isVisible])
return (
isVisible &&
<FormControl component='fieldset'>
<FormLabel component='legend'>Select type of material:</FormLabel>
<RadioGroup name='MaterialTypes' value={value} onChange={handleChange}>
<FormControlLabel
value='Mystery'
control={<Radio />}
label='Mystery'
/>
<FormControlLabel
value='NonFiction'
control={<Radio />}
label='Non-fiction'
/>
</RadioGroup>
</FormControl>
)
}
export default TypeOfMaterial
Finally, we write World War II, in the text field. The data is saved in its local and global sate.
Component: SubjOfInterest.js
imports...
import React, { useState, useEffect } from 'react'
import TextField from '#material-ui/core/TextField'
const SubjOfInterest = ({ formState, setFormState, ageLevel, materialType }) => {
const [textField, setTextField] = useState('')
const [isVisible, setIsVisible] = useState('')
const handleTxtFldChange = (e) => {
setTextField(e.target.value)
setFormState({ ...formState, subjInterest: e.target.value })
}
useEffect(() => {
if (formState.materialType === 'NonFiction') {
setIsVisible(true)
} else {
setIsVisible(false)
}
}, [materialType])
useEffect(() => {
if (formState.materialType !== 'NonFiction') {
setTextField('')
}
}, [ageLevel])
return (
isVisible &&
<TextField
value={textField}
onChange={e => handleTxtFldChange(e)}
label='Indicate subjects of interest:'
/>
)
}
export default SubjOfInterest
At this point the global state looks as follow:
{
fullName:"Jhon Doe",
ageLevel:"Adults",
numTitle:"Two",
materialType:"NonFiction",
subjInterest:"World War"
}
Then if a user changes the selected option (Adults) from the AgeLeveleSelect to a different option (teens for example) the a part of global state (numTitle, materialType, subjInterest) is expected to be cleared, instead I get this:
{
fullName:"Jhon Doe",
ageLevel:"Teens",
numTitle:"Two",
materialType:"",
subjInterest:"World War"
}
Any ideas?
I have tried many things without results.
If anyone can help will be greatly appreciated!!!
You are only clearing the materialType field:
On the NumOfTitles.js file you are setting the field "materialType" instead of the "numOfTitles" field.
On the SubjOfInterest.js you are not clearing any fields.
My suggestion is that you check the "Adults" condition on the parent component. This way you will not update the same state three times (if this occurs at the same time, this can cause some problems).
You can try doing this way:
App.js
function App () {
const [formState, setFormState] = useState({
fullName: '',
email: '',
ageLevel: '',
numTitle: '',
titleTypes: '',
materialType: '',
subjInterest: ''
})
useEffect(() => {
if(formState.ageLevel !== 'Adults') {
setFormState({
...formState,
numTitle: '',
materialType: '',
subjInterest: '',
})
}
}, [formState.ageLevel]);
// ...

have problem with react useEffect function

I have this update form for a place and I fetch its data from the backend to add initial inputs in useEffect but I got this error
Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
I know the problem is related to unmounted the component before update the state but I try many solutions but not working. Anyone have an idea how to fix that
const UpdatePlace = () => {
const placeId = useParams().pId;
const [loadedPlace, setLoadedPlace] = useState();
// const [isLoading, setIsLoading] = useState(true);
const { error, sendRequest, clearError } = useHttpClient();
const [isLoading, formState, inputHandler, setFormData] = useForm(
{
title: {
value: "",
isValid: false,
},
description: {
value: "",
isValid: false,
},
},
true
);
useEffect(() => {
const fetchPlace = async () => {
try {
const res = await sendRequest(`/api/places/${placeId}`);
await setLoadedPlace(res.data.place);
setFormData(
{
title: {
value: res.data.place.title,
isValid: true,
},
description: {
value: res.data.place.description,
isValid: true,
},
},
true
);
} catch (err) {}
};
fetchPlace();
}, [sendRequest, placeId, setFormData]);
if (!loadedPlace && !error) {
return (
<div className="center" style={{ maxWidth: "400px", margin: "0 auto" }}>
<Card>
<h2>No place found!</h2>
</Card>
</div>
);
}
const placeUpdateSubmitHandler = (e) => {
e.preventDefault();
console.log(formState.inputs, formState.isFormValid);
};
return (
<>
{isLoading ? (
<LoadingSpinner asOverlay />
) : error ? (
<ErrorModal error={error} onClear={clearError} />
) : (
<>
<Title label="Update place" />
<form className="place-form" onSubmit={placeUpdateSubmitHandler}>
<Input
element="input"
type="text"
id="title"
label="Update title"
validators={[VALIDATOR_REQUIRE()]}
errorText="please enter valid title"
onInput={inputHandler}
initialValue={loadedPlace.title}
initialValid={true}
/>
<Input
element="textarea"
id="description"
label="Update description"
validators={[VALIDATOR_REQUIRE(), VALIDATOR_MINLENGTH(5)]}
errorText="please enter valid description (min 5 chars) "
onInput={inputHandler}
initialValue={loadedPlace.description}
initialValid={true}
/>
<Button type="submit" disabled={!formState.isFormValid}>
Update place
</Button>
</form>
</>
)}
</>
);
};
You can use useEffect with [] with cleanup function, as it will execute last one like this:
useEffect(() => {
return () => {
console.log('cleaned up');
}
},[])
This error means that your request completes after you have navigated away from that page and it tries to update a component that is already unmounted. You should use an AbortController to abort your request. Something like this should work:
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
const fetchPlace = async () => {
try {
const res = await fetch(`/api/places/${placeId}`, { signal }).then(response => {
return response;
}).catch(e => {
console.warn(`Fetch 1 error: ${e.message}`);
});
await setLoadedPlace(res.data.place);
setFormData(
{
title: {
value: res.data.place.title,
isValid: true,
},
description: {
value: res.data.place.description,
isValid: true,
},
},
true
);
} catch (err) {}
};
fetchPlace();
return () => {
controller.abort();
};
}, [sendRequest, placeId, setFormData]);
Edit: Fix undefined obj key/value on render
The above warning will not stop your component from rendering. What would give you an undefined error and prevent your component from rendering is how you initiate the constant loadedPlace. You initiate it as null but you use it as an object inside your Input initialValue={loadedPlace.title}. When your component tries to do the first render it reads the state for that value but fails to locate the key and breaks.
Try this to fix it:
const placeObj = {
title: {
value: '',
isValid: true,
},
description: {
value: '',
isValid: true,
};
const [loadedPlace, setLoadedPlace] = useState(placeObj);
Always make sure that when you use an object you don't use undefined keys upon render.

How to use prevState so I don't have to use a setTimeout

How to use prevState on 'this.state.lat' & 'this.state.lng' so that I can remove the setTimeout from this peice of code below:
getCords(authUser) {
setTimeout(() => {
this.props.firebase.cats().push({
text: this.state.text,
image: 'https://images.unsplash.com/photo-1518791841217-8f162f1e1131?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9',
userId: authUser.uid,
lat: this.state.lat,
lng: this.state.lng,
})
this.setState({ ...INITIAL_STATE });
}, 150)
}
Without the setTimeout I get the incorrect values, so I think using prevState in replace of setTimeout should fix this issue? Any pointers are much appreciated!
Full code:
import React from 'react';
import { AuthUserContext, withAuthorization } from '../Session';
import Geocode from "react-geocode";
const INITIAL_STATE = {
text: '',
lat: '',
long: '',
address: ''
}
class AddCat extends React.Component {
constructor(props) {
super(props);
this.state = {
...INITIAL_STATE
}
}
componentDidMount() {
// set Google Maps Geocoding API for purposes of quota management. Its optional but recommended.
Geocode.setApiKey(process.env.REACT_APP_GOOGLEGEOCODEKEY);
// set response language. Defaults to english.
Geocode.setLanguage("en");
// set response region. Its optional.
// A Geocoding request with region=es (Spain) will return the Spanish city.
Geocode.setRegion("es");
// Enable or disable logs. Its optional.
Geocode.enableDebug();
// Get latidude & longitude from address.
}
getCords(authUser) {
setTimeout(() => {
this.props.firebase.cats().push({
text: this.state.text,
image: 'https://images.unsplash.com/photo-1518791841217-8f162f1e1131?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9',
userId: authUser.uid,
lat: this.state.lat,
lng: this.state.lng,
})
this.setState({ ...INITIAL_STATE });
}, 150)
}
onCreateCat = (e, authUser) => {
Geocode.fromAddress(this.state.address).then(
response => {
const { lat, lng } = response.results[0].geometry.location;
this.setState({lat: lat, lng: lng},
this.getCords(authUser)
);
},
error => {
console.error(error);
}
)
e.preventDefault();
}
onChangeText = e => {
this.setState({ text: e.target.value });
};
onChangeAddress = e => {
this.setState({ address: e.target.value });
};
render() {
console.log(this.state);
return (
<div>
<h1>Add cat</h1>
<AuthUserContext.Consumer>
{authUser => (
<div>
<form onSubmit={e => this.onCreateCat(e, authUser)}>
<input
type="text"
value={this.state.text}
onChange={this.onChangeText}
placeholder="Cats Name"
/>
<input
name="address"
value={this.state.address}
onChange={this.onChangeAddress}
type="text"
placeholder="Cats Postcode">
</input>
<button type="submit">Send</button>
</form>
</div>
)}
</AuthUserContext.Consumer>
</div>
);
}
}
const condition = authUser => !!authUser;
export default withAuthorization(condition)(AddCat);
If you look at the signature of the setState method it takes an updater and a callback that will run after the state gets updated:
setState(updater[, callback])
So you could call your this.getCords function inside of a this.setState callback, like so:
onCreateCat = (e, authUser) => {
e.preventDefault()
Geocode.fromAddress(this.state.address).then(
response => {
// ...
this.setState({ lat, lng }, () => {
// call methods of your component
// that rely on latest state values
// for `lat` and `lng` here
this.getCords(authUser)
})
}
)
}
Then, inside of the getCords method, you'd just do whatever you need to do without introducing "hacks":
getCords = (authUser) => {
this.props.firebase.cats().push(...)
this.setState({ ...INITIAL_STATE })
}
Here's a quick demo:
CodeSandbox
Hope this helps.

Resources