Input onChange doesn't fire when deleting sessionStorage values (Next.js) - javascript

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

Related

Handling data rendering on redux state change

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

How to observe change in a global Set variable in useEffect inside react component function?

I have a page wherein there are Listings.
A user can check items from this list.
Whenever the user checks something it gets added to a globally declared Set(each item's unique ID is added into this set). The ID's in this set need to be accessed by a seperate Component(lets call it PROCESS_COMPONENT) which processes the particular Listings whose ID's are present in the set.
My Listings code roughly looks like:
import React from "react";
import { CheckBox, PROCESS_COMPONENT } from "./Process.jsx";
const ListItem = ({lItem}) => {
return (
<>
//name,image,info,etc.
<CheckBox lId={lItem.id}/>
</>
)
};
function Listings() {
// some declarations blah blah..
return (
<>
<PROCESS_COMPONENT /> // Its a sticky window that shows up on top of the Listings.
//..some divs and headings
dataArray.map(item => { return <ListItem lItem={item} /> }) // Generates the list also containing the checkboxes
</>
)
}
And the Checkbox and the PROCESS_COMPONENT functionality is present in a seperate file(Process.jsx).
It looks roughly like:
import React, { useEffect, useState } from "react";
let ProcessSet = new Set(); // The globally declared set.
const CheckBox = ({lID}) => {
const [isTicked, setTicked] = useState(false);
const onTick = () => setTicked(!isTicked);
useEffect( () => {
if(isTicked) {
ProcessSet.add(lID);
}
else {
ProcessSet.delete(lID);
}
console.log(ProcessSet); // Checking for changes in set.
}, [isTicked]);
return (
<div onClick={onTick}>
//some content
</div>
)
}
const PROCESS_COMPONENT = () => {
const [len, setLen] = useState(ProcessSet.size);
useEffect( () => {
setLen(ProcessSet.size);
}, [ProcessSet]); // This change is never being picked up.
return (
<div>
<h6> {len} items checked </h6>
</div>
)
}
export { CheckBox, PROCESS_COMPONENT };
The Set itself does get the correct ID values from the Checkbox. But the PROCESS_COMPONENT does not seem to be picking up the changes in the Set and len shows 0(initial size of the set).
I am pretty new to react. However any help is appreciated.
Edit:
Based on #jdkramhoft
's answer I made the set into a state variable in Listings function.
const ListItem = ({lItem,set,setPSet}) => {
//...
<CheckBox lID={lItem.id} pset={set} setPSet={setPSet} />
)
}
function Listings() {
const [processSet, setPSet] = useState(new Set());
//....
<PROCESS_COMPONENT set={processSet} />
dataArray.map(item => {
return <ListItem lItem={item} set={processSet} setPSet={setPSet} />
})
}
And corresponding changes in Process.jsx
const CheckBox = ({lID,pset,setPSet}) => {
//...
if (isTicked) {
setPSet(pset.add(lID));
}
else {
setPSet(pset.delete(lID));
}
//...
}
const PROCESS_COMPONENT = ({set}) => {
//...
setLen(set.size);
//...
}
Now whenever I click the check box I get an error:
TypeError: pset.add is not a function. (In 'pset.add(lID)', 'pset.add' is undefined)
Similar error occurs for the delete function as well.
First of all, the set should be a react state const [mySet, setMySet] = useState(new Set()); if you want react to properly re-render with detected changes. If you need the set to be available to multiple components you can pass it to them with props or use a context.
Secondly, React checks if dependencies like [ProcessSet] has been changed with something like ===. Even though the items in the set are different, no change is detected because the object is the same and there is no re-render.
Update:
The setState portion of [state, setState] = useState([]); is not intended to mutate the previous state - only to provide the next state. So to update your set you would do something like:
const [set, setSet] = useState(new Set())
const itemToAdd = ' ', itemToRemove = ' ';
setSet(prev => new Set([...prev, itemToAdd]));
setSet(prev => new Set([...prev].filter(item => item !== itemToRemove)));
As you might notice, this makes adding and removing from a set as slow as a list. So unless you need to make a lot of checks with set.has() I'd recommend using a list:
const [items, setItems] = useState([])
const itemToAdd = ' ', itemToRemove = ' ';
setItems(prev => [...prev, itemToAdd]);
setItems(prev => prev.filter(item => item !== itemToRemove));

React hook form method - setValue - doesn't work

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

How to update and display first non-empty value on setState with react?

I'm trying to display the value of my inputs from a from, in a list. Everytime I hit submit, I expect that it should display the inputs in order.
The problem I'm having is that when I try to submit my form and display inputs in a list, it display an empty value first. On the next submit and thereafter, it displays the previous value, not the new one on the input field.
There's also an error message but i'm not understanding how to relate it to the problem. It's a warning message regarding controlled/uncontrolled components.
I've tried to add if statements to check for empty values in each functions but the problem persists.I've tried to manage the error massage by being consistent with all input to be controlled elements using setState, but nothing works.
I looked through todo list examples on github. I guess i'm trying to keep it in one functional component versus multiple ones, and I'm not using class components. I tried to follow the wesbos tutorial on Javascript 30 day challenge, day 15: Local Storage and Event Delegation. I'm trying to use React instead of plain JS.
Here's what my component looks like.
import React, { useEffect, useState } from "react";
import "../styles/LocalStorage.css";
export const LocalStorage = () => {
const [collection, setCollection] = useState([]);
const [value, setValue] = useState();
const [item, setItem] = useState({ plate: "", done: false });
const [display, setDisplay] = useState(false);
//set the value of the input
const handleChange = (e) => {
if (e.target.value === "") return;
setValue(e.target.value);
};
const handleSubmit = (e) => {
e.preventDefault();
if (value === "" || undefined) return;
setItem((prevState) => {
return { ...prevState, plate: value };
});
addItem(item);
setDisplay(true);
setValue("");
};
const addItem = (input) => {
if (input.plate === "") return;
setCollection([...collection, input]);
};
return (
<div>
<div className="wrapper">
<h2>LOCAL TAPAS</h2>
<ul className="plates">
{display ? (
collection.map((item, i) => {
return (
<li key={i}>
<input
type="checkbox"
data-index={i}
id={`item${i}`}
checked={item.done}
onChange={() =>
item.done
? setItem((state) => ({ ...state, done: false }))
: setItem((state) => ({ ...state, done: true }))
}
/>
<label htmlFor={`item${i}`}>{item.plate}</label>
</li>
);
})
) : (
<li>Loading Tapas...</li>
)}
</ul>
<form className="add-items" onSubmit={handleSubmit}>
<input
type="text"
name="item"
placeholder="Item Name"
required
value={value}
onChange={handleChange}
/>
<button type="submit">+ Add Item</button>
</form>
</div>
</div>
);
};
Since the setState function is asynchronous, you cannot use the state value item right after you fire the setItem(...). To ensure you get the latest value for your addItem function:
setItem((prevState) => {
const newItem = { ...prevState, plate: value };
addItem(newItem); // Now, it's getting the updated value!
return newItem;
});
And regarding the controlled and uncontrolled components, you can read the docs about it here. To fix your problem, you can initialize the value state with an empty string:
const [value, setValue] = useState('');

Working with react input field says synthetic even when i try and save, and setting value instead of defaultValue is rendering [Object, object]

Disclaimer I am new to developing. I am having trouble when I try and save my changes on my input field I get an error saying
"Warning: This synthetic event is reused for performance reasons. If you're seeing this, you're accessing the property nativeEvent on a released/nullified synthetic event. This is set to null. If you must keep the original synthetic event around, use event.persist()."
Also if I set the "value" instead of the "defaultValue" when I type in the field I get [Object, object].
This is the input component:
const Profile = ({
profile,
mCatalog,
sCatalog,
isEditing,
onChange,
restoreData,
userID,
}) => {
const updateProviderNotes = (event) => {
const { name, value } = event.target;
onChange(name)(value);
}
return (
<Input
type="textarea"
disbaled={false}
name="providerNotes"
value={providerNote}
onChange={updateProviderNotes}
/>
)
const Editor = ({ source, onChange, items, oldItems, name }) => {
return (
<div className="d-flex ml-3">
<div className={styles.bubble}>
<ListEditor
items={items}
oldItems={oldItems || []}
itemListSource={source}
onChange={onChange(name)}
/>
</div>
</div>
);
};
export default Profile;
this is a portion of the parent component
const ProfileData = ({
profile,
mCatalog,
sCatalog,
page,
catalog,
userID,
setProfile,
}) => {
const [editingProfile, setEditingProfile] = useState(false);
const [oldProfile, setOldProfile] = useState(false);
useEffect(() => {
setOldProfile(profile)
}, [])
const handleMProfileCancel = () => {
setProfile(oldProfile)
}
const handleMedicalProfileSave = () => {
console.log("profile", profile)
console.log(typeof profile.medicalProfile.providerNotes)
api.UserRecords.updateMedicalProfile(userID, profile.medicalProfile)
setOldProfile(profile)
}
const updateMedicalProfileDetails = (fieldName) => (value) => {
setProfile({ ...profile, mProfile: {...profile.mProfile, [fieldName]: value }});
};
return (
{page === "medicalProfile" && (
<InfoEditWrapper
data={oldProfile.medicalProfile}
onCancel={handleMedicalProfileCancel}
onSave={handleMedicalProfileSave}
>
<Profile
profile={profile.medicalProfile}
medicalCatalog={medicalCatalog}
surgicalCatalog={surgicalCatalog}
onChange={updateMedicalProfileDetails}
userID={userID}
/>
</InfoEditWrapper>
)}
)
export default ProfileData;
Any advice would be helpful thanks!
For your warning message, I would refer to this question. You are basically getting this error because you are using your event in an asynchronous context (updating your state) which isn't allowed. You can avoid this error if you assign your event to a local variable and reference it.
if I set the "value" instead of the "defaultValue" when I type in the
field I get [Object, object]
Your onChange event handler will receive a Synthetic event object and your parameter you're passing with it. With your current code you assigned the whole event object as the field value.
Your updateMedicialProfileDetails method that you are passing as the onChange prop isn't in your question so I'm using the updateProfileDetails method as an example:
The following code should work:
const updateProfileDetails = (fieldName) => (event) => {
const { value } = event.target;
setProfile({ ...profile, mProfile: {...profile.mProfile, [fieldName]: value }});
};
Your name parameter you are passing with this function is unnecessary since your event object will have the name attribute available, so your code can be updated to the following:
<Input
type="textarea"
name="providerNotes"
value={profile.providerNotes}
onChange={onChange}
oldValue={restoreData.providerNotes}
/>
The event handler:
const updateProfileDetails = (event) => {
const { name, value } = event.target;
setProfile({ ...profile, mProfile: {...profile.mProfile, [name]: value }});
};

Categories