react-hook-form conditional form value control - reactjs

I want to delete model's value when manufacturerWatchValue changes after form is initialized.
But model loses its value upon form-initialized since initialized variable is in the dependency list.
Hence, I had to remove it from the dependency list to make it work.
Even though it works as expected, React says Line 138:5: React Hook useEffect has a missing dependency: 'initialized'. Either include it or remove the dependency array
Is there a way to bypass this error while achieving the logic?
const MyForm = ({ itemId }) => {
const { watch, setValue, handleSubmit, reset } = useForm()
const [initialized, setInitialized] = useState(false)
const manufacturerWatchValue = watch("manufacturer")
useEffect(() => {
// Initializing Form Here
itemId &&
(async () => {
const oneItem = await getOneResult(itemId)
reset(oneItem)
setInitialized(true)
})()
// Terminating Form Here
return () => {
reset({})
setInitialized(false)
}
}, [dispatch, reset, itemId])
useEffect(() => {
if (initialized) {
setValue("model", "")
}
}, [manufacturerWatchValue, setValue /*, initialized */])
const onSubmit = (data) => {
console.log(data)
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input type="text" name="model" ref={register} />
<input type="text" name="manufacturer" ref={register} />
</form>
)

React is not actually throwing an error at you, instead throwing an eslint warning at you.
You can ignore the specific linting error by adding this line above the dependency error:
// eslint-disable-next-line
Example
useEffect(() => {
console.log("My code")
// eslint-disable-next-line
}, [])

Related

React Hook Form and persistent data on page reload

I'm using React Hook Form v7 and I'm trying to make my data form persistent on page reload. I read the official RHF documentation which suggests to use little state machine and I tried to implement it but without success. Is there a better way to do it? However...
The first problem I encountered using it, is that my data is a complex object so the updateAction it should be not that easy.
The second problem is that I don't know when and how to trigger the updateAction to save the data. Should I trigger it on input blur? On input change?
Here's my test code:
If persisting in the localStorage works you, here is how I achieved it.
Define a custom hook to for persisting the data
export const usePersistForm = ({
value,
localStorageKey,
}) => {
useEffect(() => {
localStorage.setItem(localStorageKey, JSON.stringify(value));
}, [value, localStorageKey]);
return;
};
Just use it in the form component
const FORM_DATA_KEY = "app_form_local_data";
export const AppForm = ({
initialValues,
handleFormSubmit,
}) => {
// useCallback may not be needed, you can use a function
// This was to improve performance since i was using modals
const getSavedData = useCallback(() => {
let data = localStorage.getItem(FORM_DATA_KEY);
if (data) {
// Parse it to a javaScript object
try {
data = JSON.parse(data);
} catch (err) {
console.log(err);
}
return data;
}
return initialValues;
}, [initialValues]);
const {
handleSubmit,
register,
getValues,
formState: { errors },
} = useForm({ defaultValues: getSavedData() });
const onSubmit: SubmitHandler = (data) => {
// Clear from localStorage on submit
// if this doesn’t work for you, you can use setTimeout
// Better still you can clear on successful submission
localStorage.removeItem(FORM_DATA_KEY);
handleFormSubmit(data);
};
// getValues periodically retrieves the form data
usePersistForm({ value: getValues(), localStorageKey: FORM_DATA_KEY });
return (
<form onSubmit={handleSubmit(onSubmit)}>
...
</form>
)
}
I already faced this issue and implemented it by creating a custom Hook called useLocalStorage. But since you are using the React hook form, it makes the code a bit complicated and not much clean!
I suggest you simply use the light package react-hook-form-persist.
The only work you need to do is to add useFormPersist hook after useForm hook. Done!
import { useForm } from "react-hook-form";
import useFormPersist from "react-hook-form-persist";
const yourComponent = () => {
const {
register,
control,
watch,
setValue,
handleSubmit,
reset
} = useForm({
defaultValues: initialValues
});
useFormPersist("form-name", { watch, setValue });
return (
<TextField
title='title'
type="text"
label='label'
{...register("input-field-name")}
/>
...
);
}
The state itself won't persist any data on page reload.
You need to add your state data to Local Storage.
Then load it back into the state on componentDidMount (useEffect with empty dependency array).
const Form = () => {
const [formData, setFormData] = useState({})
useEffect(() => {
if(localStorage) {
const formDataFromLocalStorage = localStorage.getItem('formData');
if(formDataFromLocalStorage) {
const formDataCopy = JSON.parse(formDataFromLocalStorage)
setFormData({...formDataCopy})
}
}
}, []);
useEffect(() => {
localStorage && localStorage.setItem("formData", JSON.stringify(formData))
}, [formData]);
const handleInputsChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value
})
}
return (
<div>
<input
type="text"
name="firstName"
placeholder='first name'
onChange={e => handleInputsChange(e)}
value={formData?.firstName}
/>
<input
type="text"
name="lastName"
placeholder='last name'
onChange={e => handleInputsChange(e)}
value={formData?.lastName}
/>
</div>
)
}

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 to set focus a ref using React-Hook-Form

How do you implement set focus in an input using React-Hook-Form, this is what their FAQ's "How to share ref usage" code here https://www.react-hook-form.com/faqs/#Howtosharerefusage
import React, { useRef } from "react";
import { useForm } from "react-hook-form";
export default function App() {
const { register, handleSubmit } = useForm();
const firstNameRef = useRef();
const onSubmit = data => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input name="firstName" ref={(e) => {
register(e)
firstNameRef.current = e // you can still assign to ref
}} />
<input name="lastName" ref={(e) => {
// register's first argument is ref, and second is validation rules
register(e, { required: true })
}} />
<button>Submit</button>
</form>
);
}
I tried set focusing the ref inside useEffect but it doesn't work:
useEffect(()=>{
firstNameRef.current.focus();
},[])
Neither does inside the input:
<input name="firstName" ref={(e) => {
register(e)
firstNameRef.current = e;
e.focus();
}} />
You can set the focus using the setFocus helper returned by the useForm hook (no need to use a custom ref):
const allMethods = useForm();
const { setFocus } = allMethods;
...
setFocus('inputName');
https://react-hook-form.com/api/useform/setFocus
Are you using Typescript?
If so, replace...
const firstNameRef = useRef();
With...
const firstNameRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
if (firstNameRef.current) {
register(firstNameRef.current)
firstNameRef.current.focus()
}
}, []);
<input name="firstName" ref={firstNameRef} />
Got this from : https://github.com/react-hook-form/react-hook-form/issues/230
If you using Version 7, you can check this link in the docs
https://www.react-hook-form.com/faqs/#Howtosharerefusage
I can't comment (I don't have enough reputation), but without setting a timeout, setFocus didn't work. After adding a simple timeout from Subham's answer, it worked like a charm!
PS: Even adding a timeout of 0: setTimeout(() => setFocus(fieldName), 0) works. Can anyone explain?
I think you can simply use ref={(el) => el.focus()} to set the focus. The catch here is to make sure no other element within your page is also setting focus right after that el.focus() call.
const {
register,
handleSubmit,
setFocus, // here
formState: { errors },
} = useForm<FormFields>({
resolver: yupResolver(LoginSchema),
mode: "onTouched",
});
useEffect(() => {
setFocus("email");
}, [setFocus]);

React: socket.io and useEffect not working properly

So I'm learning socket.io with react, and I'm encountering an issue. What I want to happen is that when a user inputs something into the field and submits it, it makes the title of everyone's browser that input. However, if your text field is equal to the message spread with everyone (meaning it was probably you who clicked enter) it'll make your title 'You changed it'.
However, it seems that in io.on(message), when it checks if the input state is equal to the message, it always checks the initial value of the state against the message, which is ''.
I'm guessing this happens because useEffect is called at the beginning when the state is '', so it hard codes the io.on(message) to be what the state is at that time. I thought to fix this i'd just make input a part of useEffect's dependancy array, but that doesn't override the current io.on(message), it overloads it, so both are called. Don't know how to fix this!
import React from 'react'
import io from 'socket.io-client'
const SocketTest = (props: any) => {
const socket = io('http://localhost:4000')
const [input, setInput] = React.useState<string>('')
const handleNewMessage = (message: string) => {
console.log(input)
if (input !== message){
document.title = message
} else {
document.title = 'You changed it!'
}
}
React.useEffect(() => {
socket.on('message', (message: string) => {
handleNewMessage(message)
})
}, [])
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInput(e.target.value)
}
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
socket.emit('message', input)
}
return (
<div>
<h2 className="center">{ input }</h2>
<form onSubmit={ (e) => handleSubmit(e) }>
<input type="text" placeholder="Enter text" onChange={ (e) => handleChange(e) }/>
</form>
</div>
)
}
export default SocketTest
I'm also sure I could fix this by broadcasting the message instead of emitting it, but I'm moreso trying to learn how to fix that issue.

React hook not setting the state

Hey all trying to use a useState react hook to set a state but it does not work, I gone through the official documentation
Seems like i have followed it correctly but still cannot get the hook to set the state:
const [search, setSearch] = useState('');
const { films } = props;
const matchMovieSearch = (films) => {
return films.forEach(item => {
return item.find(({ title }) => title === search);
});
}
const handleSearch = (e) => {
setSearch(e.target.value);
matchMovieSearch(films);
}
<Form.Control
type="text"
placeholder="Search Film"
onChange={(e) => {handleSearch(e)}}
/>
Search var in useState is allways empty even when i debug and can see that e.target.value has to correct data inputed from the html field
setSearch is an async call, you won't be able to get the search immediately after setting the state.
useEffect is here for rescue.
useEffect(() => {
// your action
}, [search]);
Are you sure you are using the hooks inside a component, hooks can only be used in a Functional React Component.
If that is not the case, there must be something wrong with the Form.Control component, possibly like that component did not implement the onChanged parameter properly.
This is the one I tested with the html input element, and it is working fine. I used the useEffect hook to track the changes on the search variable, and the you can see that the variable is being properly updated.
https://codesandbox.io/s/bitter-browser-c4nrg
export default function App() {
const [search, setSearch] = useState("");
useEffect(() => {
console.log(`search was changed to ${search}`);
}, [search]);
const handleSearch = e => {
setSearch(e.target.value);
};
return (
<input
type="text"
onChange={e => {
handleSearch(e);
}}
/>
);
}

Resources