I'm trying to setup a form. It has Edit feature where on edit I call an API and get the data into state.
I'm struggling to display data in the form after api call. There's no problem utilizing the API or calling the redux functions. Problem is that my Form only displays last data in the redux state but not the updated data.
That's how I'm doing the stuff.
Calling API if isEdit===True at the same time Form is being displayed on component mount.
Updateding state after success as an object called customer
accessing the customer object like this
const { customer } = useSelector((state) => state.customers)
Lets say I have a input field where I want to display the email of customer.
I'm handling this think like that:
email: isEdit ? customer?.email : '', // At this point there is some problem
It loads the previous data that was stored in the state.customer but not the new one.
I believe my email field is rendering first and then doesn't updated the value when change happens in state.customer.
So how I can fix this? So that email value should be changed at the same time if state.customer got changed
Here is the full component. Still removed irrelevant part.
const CustomerNewEditForm = ({ isEdit, id, currentUser}) => {
const dispatch = useDispatch()
const navigate = useNavigate()
console.log('isEdit', isEdit, 'id', id, 'currentUser', currentUser)
// get sales reps
const { customer } = useSelector((state) => state.customers)
// const customer = () => {
// return isEdit ? useSelector((state) => state.customers?.customer) : null
// }
const { enqueueSnackbar } = useSnackbar()
const defaultValues = useMemo(
() => ({
email: isEdit ? customer?.email : '',
name: isEdit ? customer?.name : '',
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[currentUser]
)
const methods = useForm({
resolver: yupResolver(NewUserSchema),
defaultValues
})
const {
reset,
watch,
control,
setValue,
handleSubmit,
formState: { isSubmitting }
} = methods
const values = watch()
useEffect(() => {
if (isEdit === true) {
dispatch(getCustomerDetails(id))
console.log(customer)
}
if (isEdit && currentUser) {
reset(defaultValues)
}
if (!isEdit) {
reset(defaultValues)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isEdit, currentUser])
const onSubmit = async () => {
try {
await new Promise((resolve) => setTimeout(resolve, 500))
reset()
let body = {
email: values.email,
name: values.name,
}
console.log(body)
dispatch(createCustomer(body))
enqueueSnackbar(!isEdit ? 'Create success!' : 'Update success!')
// navigate(PATH_DASHBOARD.admin.root)
} catch (error) {
console.error(error)
}
}
return (
<FormProvider methods={methods} onSubmit={handleSubmit(onSubmit)}>
<Grid item md={3}>
{' '}
<RHFTextField name="name" label="Customer Name" />
</Grid>
<Grid item md={3}>
{' '}
<RHFTextField name="email" label="Email Address" />
</Grid>
</FormProvider>
)
}
export default CustomerNewEditForm
Here in the component defaultValues carries the previous data from customer object if its True and renders the form with those values. but new data comes a miliseconds later but form renders first.
First of all try to console.log your customer data and make sure that it gets a fresh data on last render.
If it gets fresh data, try take a look at your Input component, it might set some initial data, so the input will be editable and controlled by some state.
Try to modify your input's state on redux store update in useEffect.
Currently that's all that I can suggest, update your post with code with your form and input, also post your console.log result, if my answer doesn't helped you.
If the problem would be not in form\input state and console.log wouldn't show you actual updated data in last render, then I will need to see your redux store code to resolve this issue.
Hope it helped
Related
I'm loading data with a custom hook from the session storage into an input, and if I delete the whole field, the onChange() function doesn't trigger. If I only add or delete one character, it works fine, but if I select all (or if the input had only one character), then delete it doesn't seem to do anything.
This only applies, when I delete the content after render, without doing anything else in the input beforehand.
//this works fine
const [test, setTest] = useState('test')
<input value={test} onChange={(e) => setTest(e.target.value)} />
//this doesn't trigger, when deleting all content after rendering the default value
const [test2, setTest2] = useSessionStorage({key: 'test2', defaultValue: 'test2'})
<input value={test2} onChange={(e) => setTest2(e.target.value)} />
Here is my custom hook:
export const useSessionStorage = (hookProps) => {
const { key, defaultValue } = hookProps
const [sessionItem, setSessionItem] = useState(() => {
if (typeof window != 'undefined') {
const item = sessionStorage.getItem(key)
return item ? JSON.parse(item) : defaultValue
}
})
useEffect(() => {
sessionStorage.setItem(key, JSON.stringify(sessionItem))
}, [key, sessionItem])
return [sessionItem, setSessionItem]
}
I'm sure it has to do something with the session storage loading in the server first, or just after the first render, but I have no solution.
Your code is working fine. In fact, I did not detect the render when test value changes. I added this useEffect inside the component,
useEffect(() => {
console.log("I am rerendering because test value is changing");
}, [test]);
inside useSessionStorage useEffect, add this
useEffect(() => {
console.log("I am rerendering becasue test2 value is changing");
sessionStorage.setItem(key, JSON.stringify(sessionItem));
}, [key, sessionItem]);
Now test it
SOLVED
The session storage loads from the server side, so it gave undefined (empty string) at first, and the actual value after. The input got the empty string, and when trying to change it to '', the onChange() didn't trigger, due to no changes.
Writing the input in a component and disabling SSR at import works.
import dynamic from 'next/dynamic'
const DynamicInput = dynamic(() => import('../components/Input'), {
ssr: false
})
export default function Page(){
const [test, setTest] = useSessionStorage({ key: 'test', defaultValue: null })
return (
<DynamicInput value={test} onChange={(e) => setTest(e.target.value)} />
)
}
I have a notes state array that I have that stores the user's inputs. When a user initially opens this specific screen, the component should fetch the user's notes, and return that array or an empty one depending on if they have data or not, and display the notes on the screen if they exist. When they add a note, the component should push this new note to the notes array and call AsyncStorage.setItem to store the new array. The component should then re-render with the state variable changing.
When I run this code, though, nothing happens. My state does not seem to update, and even though I submit text, the screen does not re-render, nor does any new text appear in the section it is supposed to appear in. Anyone know where I went wrong?
UPDATE: I have added the full code block here
const [notes, setNotes] = React.useState(null);
let getNotes = async () => {
try {
let json = await AsyncStorage.getItem(`${id}-notes`);
if (json != null) {
setNotes(JSON.parse(json));
} else {
setNotes([]);
}
} catch (e) {
console.log(e);
}
}
React.useEffect(() => {
console.log(notes, '- Has changed');
getNotes();
}, [notes]);
// user input check
<TextInput
style={styles.text}
value={note}
onChangeText={text => {setNote(text)}}
onSubmitEditing={event => {
if (event.nativeEvent.text) {
setNotes([...notes, event.nativeEvent.text]);
AsyncStorage.setItem(`${id}-notes`, JSON.stringify(notes), (e) => {});
setVisible(false);
}
}}
multiline={true}
returnKeyType='go'
/>
// what i want the screen to render
{notes && notes.map(note => {
<Note note={note} />
})}
I don't think the problem is that the state is not being set,
I think the problem is that you aren't returning anything from you map function.
Either add the return keyword.
Like this:
{notes && notes.map(note => {
return <Note note={note} />
})}
Or simply remove the curly-brackets to turn the statement into an "implicit return".
Like this:
{notes && notes.map(note => (
<Note note={note} />
))}
I have an edit caption of a post feature on my app. I am fetching a get request and then save that data in a state that will display on screen. The problem here is that it take a few second to load that data on screen. After editing the caption, which I have a onChange function that will update the state with the new edited caption. The new edited caption is followed by a put request that will update the caption of the post.
The problem here is that I am getting an error saying to change it to either a controlled or uncontrolled state. What is the right way of doing it?
Edit: For some reason, the error isn't showing on the console now with the same code I had previously without changes. So I guess that problem is fixed? But like I mentioned before, the caption would show up empty and then show the value of the caption 2 seconds after. Is there a way to show it instantly? Im assuming it not possible since It need to make a get request first and then store the data in the state, right?
The error I'm getting
Input elements should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled input element for the lifetime of the component.
Below is my code
const PostDetail = () => {
const navigate = useNavigate();
const [inputs, setInputs] = useState({ caption: "" });
const { id } = useParams();
// once data is fetched, update the edited caption to the post
useEffect(() => {
const fetchDetails = async () => {
const res = await api.get(`/post/${id}`)
.catch((error) => console.log(error.message))
const data = await res.data;
return data;
}
fetchDetails()
.then((data) => {
setInputs({
caption: data.post.caption
});
});
}, [id]);
const sendRequest = async () => {
const res = await api.put(`/post/update/${id}`, {
caption: inputs.caption
})
.catch(error => console.log(error));
const data = await res.data;
return data;
}
const handleSubmit = (e) => {
e.preventDefault();
console.log(inputs);
sendRequest()
.then((data) => console.log(data))
.then(() => navigate('/myposts'))
}
const handleChange = (e) => {
setInputs({ caption: e.target.value })
}
return (
<div>
{inputs &&
<form onSubmit={handleSubmit}>
<Box display="flex" flexDirection="column" alignItems="center" justifyContent="center" boxShadow=" 10px 10px 20px #ccc" maxWidth={500} margin="auto" padding={3} marginTop={10} borderRadius={5}>
<Typography fontWeight={"bold"} padding={3} textAlign={"center"} fontSize={22} fontFamily="georgia">Edit Your Post</Typography>
<InputLabel sx={{ mb: 1, mt: 2, fontSize: 15 }}>Caption</InputLabel>
<TextField value={inputs.caption} name="caption" onChange={handleChange} fullWidth />
<Button variant="contained" type="submit" color="warning" sx={{ marginTop: 3 }}>Submit</Button>
</Box>
</form>
}
</div>
)
}
This warning occurs when an input's value switches from null or undefined to a real value or vice-versa.
My guess is that data.post.caption isn't present in the fetch response, so you end up setting the input's value to undefined.
This makes it an uncontrolled input.
Then you type in the field, state gets updated, and you set the input's value to a string, making it a controlled input.
Hence the warning.
If that's the case you could mitigate it by ORing it with an empty string: data.post.caption || "".
I have some clearable select, and I want to reset the applets field in state to an empty array.
const defaultFormValues = { device: { ...initialDevice }, applets: [] };
const { control, getValues, setValue, reset, handleSubmit } = useForm<CreateDeviceFormData>({
mode: "all",
reValidateMode: "onChange",
defaultValues: defaultFormValues,
resolver: yupResolver(validationSchema),
});
const onChangeHandler = React.useCallback(
(value: Experience | null) => {
if (value) {
setValue("applets", getApplets(value));
} else {
setValue("applets", []);
// reset(defaultFormValues);
}
setValue("device.experience_id", value ? value.id : undefined);
},
[templateSelector, setValue],
);
console.log("current data", getValues(), control);
return (
<>
<SomeAutocompleteComponent control={control} onChange={onChangeHandler} />
<SelectAppletsComponent control={control} />
</>
);
export const SelectAppletsComponent = ({ control, onChange }) => {
const applets = useWatch({ control, name: "applets" }) as Applet[];
const device = useWatch({ control, name: "device" }) as Device;
if (!applets.length) {
return null;
}
return (
<SpanWrapper className="p-col-8">
{applets.map((applet) => (
<LabelRadio
key={applet.id}
inputId={applet.applet_type}
value={applet.applet_type}
label={applet.name}
checked={device.applet_type === applet.applet_type}
onChange={onChange}
/>
))}
</SpanWrapper>
);
};
the problem is that clearing the selection on UI with setValue("applets", []); not working for some reason, and I don't understand why, and how to do it without reset method, which resets the whole state, not just single property as I understand
You should always register fields if you want to use them as RHF's form state.
React.useEffect(() => {
register("applets");
}, [register]);
This fixes an issue.
Update:
Also a new method resetField is available
Just to follow up on this, it is indeed the right solution provided by AuthorProxy.
Using defaultValues doesn't register the fields (it seems that they are still added to the formData on submit, but since they are not registered, any user triggered changes to these fields won't reflect on the formData).
You have to register every field you want the user to be able to interact with.
We usually register fields via inputs in the JSX, but we also need to register the array since there is no input for it in the JSX.
As per shown by the author of the react hook form library.
https://github.com/react-hook-form/react-hook-form/discussions/3160
And sandbox
https://codesandbox.io/s/inspiring-wood-4z0n0?file=/src/App.tsx
I am trying to build a registration page using react hook helper.Unable to use SetForm for storing the base64 string generated. Able to retain the state by going next and previous as well.Thank you for your responses.
The link to the project : https://codesandbox.io/s/billowing-cherry-yvj90?file=/src/Second.js
MultiStepForm.js
import { useForm, useStep } from "react-hooks-helper"
const defaultData = {
FIRST_NAME: "",
LAST_NAME: "",
PHOTO: "",
const steps = [
{ id: 'details' },
{ id: 'photo' },
]
const RegistrationMultiStepForm = () => {
const [formData, setForm] = useForm(defaultData);
const props = { formData, setForm, navigation }
const { step, navigation } = useStep({
steps,
initialStep: 0
})
switch (step.id) {
case 'details':
return <Registration_First {...props} />
case 'photo':
return <Registration_Second {...props} />
}
Registration_First.js
const Registration_First = ({ formData, setForm, navigation }) => {
const { FIRST_NAME, LAST_NAME } = formData;
<input
placeholder='Enter your details'
type='text'
name="FIRST_NAME"
onChange={setForm}
value={FIRST_NAME} />
<button onClick={() => navigation.next()}
Registration_Second.js (here I am unbale to set the value for photo..). The {result} is the base64 image string which I am trying to add to my state i.e setForm for assigning the value of PHOTO field.
configuration same as Registration_First
const Cropped = () => {
// this does not work which is the correct way
setForm((previousState) => {
previousState.PHOTO = result
return previousState
})
//this works...wrong way
formData.PHOTO = result;
}
What i expect": On update of {result} value with the base64 string, i want to call the setForm action and pass the value of {result} to my PHOTO field.
After seeing the code sandbox I found out what dependency the hook comes from: react-hooks-helper https://www.npmjs.com/package/react-hooks-helper
React Hooks Helper is supposed to be used roughly something like this:
const [{ FIRST_NAME, PHOTO }, setValue] = useForm()
...
<input name='FIRST_NAME' value={FIRST_NAME} onChange={setValue} />
In your case your using an onCrop event to set the value of the PHOTO to a data url when the user is done with the canvas ui stuff.
cosnt Cropped = () => {
formData.PHOTO = result
}
This is incorrect and an anti-pattern. State should be immutable and not be changed directly. That's why the state doesn't show the update untill then next time the state is changed elsewhare. You should use a setter instead so React can update itself.
ANSWER: since were using the react-hook-helpers library we need to pass in the data it expects. We cant pass in a string we have to pass in something that looks like the form onChange argument object.
const Cropped = () => {
setForm({
target: {
name: 'PHOTO', // form element
value: result // the data/url
}
})
}
Once you do this you should see that the state will update automatically.
You can see what the library code is doing here: https://github.com/revelcw/react-hooks-helper/blob/develop/src/useForm.js
Note: For what its worth they haven't updated the library in 2 years. But it looks like a decent lib with documentation and what not. So that's nice.