Migration from v6 to v7 - react-hook-form

I have upgraded to v7 of react-hook-form today. And all went fine until i came across some legacy code using ref attribute.
In version 6 this worked perfectly
<ToggleSwitch toggleName='ifMonitoring'
ref={(e) => {
monitoring.current = e;
register(e);
}}
/>
But in version 7 ref is not used anymore, instead its {...register('ifMonitoring')}. This works fine accross the application but the above example is only one which doesnt work.
I have tried to search for similar issues but to no avail.
Anyone can help?
EDIT:
Adding more code to better understand this
function Edit() => {
const monitoring = useRef(null);
return <Controller name='monitoring' control={control} render={({ field: { ref }, fieldState }) => <ToggleSwitch ref={ref} checked={portInfo.isMonitored} />} />
ToggleSwitch is component with its own state. It does have onChange but to maintain its state
const ToggleSwitch = forwardRef((props, ref) => {
const [toggleCheck, setToggleCheck] = useState(props.checked);
const handleOnChange = (e) => {
setToggleCheck((prevState) => !prevState);
if (props.onChange) {
props.onChange(props.entry._id);
}
};
return (
<div className={`toggle btn btn-sm`}>
<input type='checkbox' defaultChecked={toggleCheck} onChange={handleOnChange} ref={ref} name={`toggle`} />
<div className='toggle-group'>
<label htmlFor={`toggle`} className={`btn btn-success`}>
In Use
</label>
<label htmlFor={`toggle`} className={`btn btn-danger`}>
Not in Use
</label>
<span className={`toggle-handle btn btn-light btn-xs`}></span>
</div>
</div>
);
EDIT 2&3:
Not working v7 Codesandbox
Working v6 Codesandbox

The ref is actually still there, it's returned by register among other things that we spread.
There is an example on the React Hook Form documentation to share ref usage.
You can do like this:
const { ref, ...rest } = register("ifMonitoring");
<ToggleSwitch
{...rest}
ref={(e) => {
monitoring.current = e;
ref(e);
}}
/>;
Edit
In your specific case, you are passing the ...rest to your ToggleSwitch component, but the component does not forward these props to the inner input (except the name that you pass yourself in a prop).
The problem here comes especially from onChange that is part of the elements inside your rest variable. As you have also your custom onChange, you can combine both yours and the one of React Hook Form.
So, in your main component you can pass the rest props like this:
<ToggleSwitch
inputProps={rest}
ref={(e) => {
ref(e);
inUseRef.current = e;
}}
// [...]
/>
And in your ToggleSwitch component, you can call the onChange of RHF in your own function, pass the input props to the input, and pass your onChange function after.
const handleOnChange = (e) => {
setToggleCheck((prevState) => !prevState);
if (inputProps.onChange) {
inputProps.onChange(e);
}
};
// [...]
<input
// [...]
{...inputProps}
onChange={handleOnChange}
ref={ref}
// [...]
/>
Here is the codesandbox.

Related

Rendering new list item after adding it from a nested form. React hooks, redux, React Router V6

I am creating a list tracking app with React hooks, Redux, and Ruby on Rails. There is a List model, with a title as a string and completed as a boolean, and a ListItem model with descriptions as a string (the list item), completed boolean, and list_id as an integer.
I am using react route V6 for this and getting a little lost in re-rendering/ updating the page. Here is the breakdown of the application:
On the home screen, you can click to view all Lists and add a new list. when viewing all list each list title is displayed as a link to that list show page. The show page shows the list title, list items and a form to add another list item. Now where I am having trouble is being able to add a new list item, and it display on the page right after submission. Right now when I add a new item, and refresh the page it is not there. But if I click back to view all lists, then click that list again it shows up under the list items.
I tried using useNavigate to navigate to that list show page even though it is already on it but I am getting this error
Uncaught TypeError: Cannot destructure property 'list' of 'location.state' as it is null.
Here is all my components:
App.js
class App extends React.Component {
render(){
return (
<div className="App">
<Navbar/>
<br></br>
<Routes>
<Route path="/" element={<Home/>} />
<Route path="/lists" element={<Lists />} />
<Route path="/lists/new" element={<ListForm />} />
<Route path="/lists/:id" element={<ListContainer />} />
</Routes>
</div>
);
}
}
Lists.js
export default function Lists() {
const lists = useSelector(state => state.lists)
// replaces mapStateToProps
const dispatch = useDispatch()
// replaces mapDispatchToProps
useEffect(() => {
dispatch(fetchLists())
}, [])
return (
<div>
{Array.isArray(lists) && lists.map((list) => {
return (
<Link
key={list.id}
to={`/lists/${list.id}`}
state={{ list: list }}
>
<h2>{list.title}</h2>
</Link>
)
})}
</div>
)
}
ListContainer.js
export default function ListContainer() {
const location = useLocation();
const { list } = location.state;
console.log(list)
return (
<div>
<List list={list}/>
<ListItemForm list={list}/>
</div>
);
}
List.js
export default function List({list}) {
return (
<div>
<h4>{list.title}</h4>
{list.list_items.map((item) => {
return (
<div key={item.id}>
<li key={item.id}>{item.description}</li>
</div>
);
})}
<br></br>
</div>
);
}
and ListItemForm.js
export default function ListItemForm({list}) {
const [item, setItem] = useState("")
const dispatch = useDispatch()
const navigate = useNavigate()
function handleSubmit(e) {
e.preventDefault()
let newItem = {description: item, completed: false, list_id: list.id}
dispatch(createListItem(newItem, list.id))
setItem("")
navigate(`/lists/${list.id}`)
}
return (
<div>
<br></br>
<form onSubmit={handleSubmit}>
<label>Add to your list: </label>
<input value={item} onChange={(e) => setItem(e.target.value)} />
</form>
</div>
)
}
I have been stuck on this for quite some time now and not sure where to go from here or where I am going wrong. Any help is appreciated!!
Sometimes when you navigate to "/lists/:id" you send route state, sometimes you don't. It's undefined when you navigate to "/lists/:id" when adding new list items. This navigation to the route you are already on for editing a list is unnecessary.
Since you are using Redux I don't think there's any need to send a list item in route state at all. Use the id route parameter and your lists redux state to derive the specific list you want to view/edit.
Example
Given: <Route path="/lists/:id" element={<ListContainer />} />
Lists
function Lists() {
const dispatch = useDispatch();
const lists = useSelector((state) => state.lists);
useEffect(() => {
if (!lists.length) {
dispatch(fetchLists());
}
}, [dispatch, lists]);
return (
<div>
{lists.map((list) => (
<Link key={list.id} to={`/lists/${list.id}`}>
<h2>{list.title}</h2>
</Link>
))}
</div>
);
}
ListContainer
import { useParams } from 'react-router-dom';
function ListContainer() {
const { id } = useParams();
const lists = useSelector((state) => state.lists);
const list = lists.find((list) => list.id === id);
return (
<div>
<List list={list} />
<ListItemForm list={list} />
</div>
);
}
ListItemForm
function ListItemForm({ list }) {
const [item, setItem] = useState("");
const dispatch = useDispatch();
function handleSubmit(e) {
e.preventDefault();
dispatch(actions.createListItem(item, list.id));
setItem("");
}
return (
<div>
<br></br>
<form onSubmit={handleSubmit}>
<label>Add to your list: </label>
<input value={item} onChange={(e) => setItem(e.target.value)} />
</form>
</div>
);
}

react-hook-form: how to remove the item (which is mentioned in defautvalue) from submit data if its not mounted. Tried unregister its not working

I have simple form with firstName and lastName. I have added some defaultValues to them in useForm. I havent mounted the lastName using if condition.
Now when I try to submit, I am expecting it to show only the mounted components values i.e firstName. But it shows both firstName and lastName. I have created a button to try to unregister("lastName") but even after unregister and not being mounted it shows in the submit data.
If I dont provide defaultValues then it works well, like if not mounted initially it will not be shown in the submit data.
Below is the code
import React, { useState } from "react";
import ReactDOM from "react-dom";
import { useForm } from "react-hook-form";
import "./styles.css";
const App = () => {
const { register, handleSubmit, unregister, watch } = useForm({
defaultValues: {
firstName: "test",
lastName: "test2"
}
});
const onSubmit = (data) => {
console.log("ON SUBMIT");
console.log(JSON.stringify(data, null, 4));
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label>Last Name</label>
<input {...register("firstName")} />
{false && (
<>
<label>Last Name</label>
<input {...register("lastName")} />
</>
)}
<button
type="button"
onClick={() => {
console.log("unregistering lastName")
unregister("lastName");
}}
>
unregister lastName
</button>
<input type="submit" />
</form>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
And the codesandbox
For the first, you can't use unregister("lastName"); before register("lastName"); it's useless. Second, when you use defaultValues, method unregister does not remove them but simply set values to default. You just can filter submit data by conditions without unregister. Demo

How to register 'react-bootstrap-typeahead' component using React 'useForm' hook?

I'm building a simple html form using "react-hook-form" library: https://react-hook-form.com/
I've incorporated "react-bootstrap-typeahead" into the html form but haven't been able to register this component with 'useForm' hook. Hence, "react-bootstrap-typeahead" input data is ignored during onSubmit.
"react-bootstrap-typeahead" doesn't provide a "name" prop which makes it difficult to register the component.
I've read the 'useForm' documentation on the different options for registering this type of components but still don't understand how to achieve this: https://react-hook-form.com/get-started#Registerfields
Does anybody have faced such challenge before?
It would be great to see a working example to get a better idea on how to implement "react-bootstrap-typeahead" + "react-hook-form" in my application. Thanks!
Here's my sample code:
import useForm from 'react-hook-form';
import { Typeahead } from 'react-bootstrap-typeahead';
import 'react-bootstrap-typeahead/css/Typeahead.css';
const myForm = (props) => {
const { register, handleSubmit, errors } = useForm();
const onSubmit = data => {
// api post request with form data
})
};
const mydata = [ "one", "two", "three" ];
return (
<>
<form onSubmit={handleSubmit(onSubmit)} >
<div className="form-group">
{/* Here I'm registering text input using ref: */}
<input type="text" className="form-control" name="name" ref={register({ required: true })} />
</div>
<div className="form-group mb-0">
{/* How can I register the below component with useForm? */}
<Typeahead
id="multiple-typeahead"
clearButton
multiple
options={mydata}
/>
</div>
<button type="submit">Save</button>
</form>
</>
);
}
This is how i was able to register the component:
import useForm from 'react-hook-form';
import { useForm, Controller } from "react-hook-form";
import 'react-bootstrap-typeahead/css/Typeahead.css';
const myForm = (props) => {
const { register, handleSubmit, errors, control } = useForm();
const onSubmit = data => {
// api post request with form data
})
};
const mydata = [ "one", "two", "three" ];
return (
<>
<form onSubmit={handleSubmit(onSubmit)} >
<div className="form-group">
<input type="text" className="form-control" name="name" ref={register({ required: true })} />
</div>
<div className="form-group mb-0">
<Controller
as={Typeahead}
control={control}
name="typeahead_component"
rules={{ required: true }}
id="multiple-typeahead"
clearButton
multiple
options={mydata}
defaultValue=""
/>
</div>
<button type="submit">Save</button>
</form>
</>
);
}

Unable to access mutator functions in Wizard form page while using react-final-form

I am trying to create a Wizard form using react-final-form by referring to this code https://codesandbox.io/s/km2n35kq3v. For my use case I need some mutator functions to be used inside my form fields. This example illustrates how to do that - https://codesandbox.io/s/kx8qv67nk5?from-embed.
I am not sure how to access mutator functions in my form steps when I am using a wizard form instead of a single page form.
I tried to combine both the examples by modifying the <Form> component rendered by Wizard.js to pass in the mutators. However I cannot access these mutators in the Wizard form pages.
In Wizard.js
return (
<Form
mutators={{
// potentially other mutators could be merged here
...arrayMutators,
}}
render={({
handleSubmit,
submitting,
values,
pristine,
invalid,
form: {
mutators: {push, pop, remove},
},
}) => {
return (
<form onSubmit={handleSubmit}>
Another file index.js
<Wizard
initialValues={{ employed: true, stooge: "larry" }}
onSubmit={onSubmit}
>
<Wizard.Page>
<FieldArray name="customers">
{({ fields }) =>
fields.map((name, index) => (
<div key={name}>
<label>Cust. #{index + 1}</label>
<Field
name={`${name}.firstName`}
component="input"
placeholder="First Name"
/>
<span
onClick={() => fields.remove(index)}
style={{ cursor: "pointer" }}
>
❌
</span>
</div>
))
}
</FieldArray>
</Wizard.Page>
</Wizard>
It errors out - remove is undefined in index.js
Look at this working example: https://codesandbox.io/s/znzlqvzvnx
changes I have made:
Wizard.js
static Page = ({ children, mutators }) => {
if(typeof children === 'function'){
return children(mutators);
}
return children;
};
...
<form onSubmit={handleSubmit}>
{
// activePage
<activePage.type {...activePage.props} mutators={mutators} />
}
...
index.js (only first <Wizard.page>)
<Wizard.Page>
{
({ upper }) => (
<React.Fragment>
<div>
<label>First Name</label>
<Field
name="firstName"
component="input"
...
</div>
</React.Fragment>
)
}
</Wizard.Page>

React onChange method not working in react_on_rails

I am trying to create a sample app based on react_on_rails gem. In my react code react inbuild function like onChange or onSubmit are not working.
My HelloWorldWidget Component looks like this.
...
constructor(props, context) {
super(props, context);
_.bindAll(this, 'handleChange');
}
handleChange(e) {
const name = e.target.value;
console.log(name);
//this.props.updateName(name);
}
render() {
const { name } = this.props;
return (
<div className="container">
<h3>
Hello, {name}!
</h3>
<input className="form-control input-sm col-sm-4" type="text" onChange={this.handleChange}/>
</div>
);
}
Also if I disable server side pre-render of my component in my views/hello_world/index.html.erb file then the component is not rendering on UI.
<%= react_component("HelloWorldApp", props: #hello_world_props , prerender: false) %>
Github Repo: react-on-rails-sample-app
I'm not sure where _.bindAll method came from but the orthodox way of binding handlers is with this syntax:
this.handleChange = this.handleChange.bind(this);
If you use arrow function you don't need to bind it to the class;
handleChange = (e) => {
const name = e.target.value;
console.log(name);
//this.props.updateName(name);
}
Try to use arrow functions like this:
onChange={(e) => this.handleChange(e)}

Resources