I have been trying to submit a login request using a form with controlled input. A submit function is passed down the React components to be triggered upon onClick of a material-ui Button. This error is only thrown when I send a mutation request using Apollo Client.
index.js:1375 Warning: A component is changing an uncontrolled input of type text to be controlled. Input elements should not switch from uncontrolled to controlled (or vice versa). Decide between using a controlled or uncontrolled input element for the lifetime of the component.
From my understanding of controlled components in React's docs, input components are "controlled" by using React value and setValue state hooks in value and onChange attributes.
This is the top level Login component that contains the submit function and useMutation hook. submit is first passed down to a LoginForm component.
const Login = () => {
const [login, { data }] = useMutation(LOGIN);
console.log(data);
const submit = async form => {
console.log(form); // form object looks correct
await login({ variables: form });
};
...
<Container>
<LoginForm submit={submit} />
</Container>
This is the LoginForm component, which renders a GeneralForm component. Again, submit is passed down to GeneralForm.
const fields = [
{
id: "username",
label: "Username",
required: true,
placeholder: "example: 98sean98"
},
...
const LoginForm = props => {
const { submit } = props;
...
<Container>
<GeneralForm fields={fields} submit={submit} />
</Container>
This is the GeneralForm component.
const GeneralForm = props => {
const { fields, submit } = props;
const [form, setForm] = useState({});
useEffect(() => {
fields.forEach(field => {
form[field.id] = "";
});
setForm(form);
}, [form, fields]);
const handleChange = event => {
form[event.target.id] = event.target.value;
setForm(setForm);
};
const handleSubmit = () => {
if (validateForm(form)) { // returns a boolean indicating validity
submit(form); // trigger the submit function that is passed down from <Login />
} else {
alert("invalid form");
}
};
return (
<FormGroup>
{fields.map(field => (
<FormControl key={field.id} required={field.required}>
<InputLabel htmlFor={field.id}>{field.label}</InputLabel>
<Input
required={field.required}
id={field.id}
type={field.type ? field.type : "text"}
aria-describedby={
field.helperText ? `${field.id}-helper-text` : null
}
placeholder={field.placeholder}
value={form[field.id]}
onChange={handleChange}
/>
{field.helperText ? (
<FormHelperText id={`${field.id}-helper-text`}>
{field.helperText}
</FormHelperText>
) : null}
</FormControl>
))}
<Button type="submit" onClick={handleSubmit}>
Submit
</Button>
</FormGroup>
);
};
My dev environment
partial packages list:
"#apollo/react-hooks": "^3.1.3",
"#material-ui/core": "^4.7.0",
"#material-ui/icons": "^4.5.1",
"apollo-boost": "^0.4.4",
"graphql": "^14.5.8",
"react": "^16.10.2",
"react-dom": "^16.10.2",
Machine: MacOS X Catalina 10.15.1
The peculiar behaviour I'm observing now is that without calling the Apollo Client mutation request,
const submit = async form => {
console.log(form);
// await login({ variables: form });
};
the above error does not get triggered. So, I wonder if Apollo Client is altering my form object incorrectly in some way.
I have spent some time digging around the internet, and this resource seems to be quite helpful. Apparently, all I had to do was to switch the value attribute of Input to listening to value that is exposed by React state in the same component instead of form[field.id] passed down from another component.
// GeneralForm.js
...
const [value, setValue] = useState(form[field.id] ? form[field.id] : "");
...
<Input value={value} ... />
So, I modularised the Input component along with its parent FormControl into another file called FormInput.js, and arrived at this solution.
// FormInput.js
const FormInput = props => {
const { field, form, setForm } = props;
const [value, setValue] = useState(form[field.id] ? form[field.id] : "");
const handleChange = event => {
setValue(event.target.value);
setForm({
...form,
[event.target.id]: event.target.value
});
};
return (
<FormControl key={field.id} required={field.required}>
<InputLabel htmlFor={field.id}>{field.label}</InputLabel>
<Input
required={field.required}
id={field.id}
type={field.type ? field.type : "text"}
aria-describedby={field.helperText ? `${field.id}-helper-text` : null}
placeholder={field.placeholder}
value={value}
onChange={handleChange}
/>
{field.helperText ? (
<FormHelperText id={`${field.id}-helper-text`}>
{field.helperText}
</FormHelperText>
) : null}
</FormControl>
);
};
Then, I import FormInput into GeneralForm, passing down all necessary props.
Related
Why, when the user enters data into the form for the first time, 'users' remains an empty array, as it was, and only after the second time button is pressed, the data is written to 'setUser?
import Card from "../UI/Card";
import Button from "../UI/Button";
const UserForm = (props) => {
const [users, setUsers] = useState([]);
const [data, setData] = useState({ username: "", age: "" });
const submitHandler = (e) => {
e.preventDefault();
setUsers([...users, data]);
console.log("2", users);
props.getUsers(users);
};
return (
<Card className={classes.container}>
<div>Username</div>
<input
type="text"
onChange={(e) => {
setData({ ...data, username: e.target.value });
}}
></input>
<div>Age (Years)</div>
<input
type="number"
onChange={(e) => setData({ ...data, age: e.target.value })}
></input>
<Button onClick={submitHandler}>Add User</Button>
</Card>
);
};
export default UserForm;
....................
React State update takes time. So you need a useEffect hook with state as a argument in it. so whenever the state change, this hook triggers and perform your state related functions.
useEffect(() => {
console.log(users)
props.getUsers(users);
}, [users]);
it does change but how react works ?
when you update the value of one react hook and if this value is different from the previous one the component re-render with the new value of the hook.
in you code you are trying to :
console.log("2", users);
this is just before the component re-render so the new value is not available yet, but directly after submitHandler the component will re-render with new value of users
you can understand better if you try to console log() from inside your jsx
return (
<Card className={classes.container}>
{console.log('this is a new render and this is my users value :',users)}
</Card>
);
learn more here about React component lifecycle
I have an issue where I have multiple input fields with user's data from the database and he can edit them. The form is working fine, although, even when nothing is changed, the submit button is enabled. How do I disable it?
Here is the function part of my code:
const handleUpdateData = async (ProfileData: any) => {
const {
name,
surname,
description,
company,
} = ProfileData;
const dataUsername = {
name,
surname,
company,
description,
}
await mutateAsync({
userId: user?._id,
dataUsername
}).then(res => {
console.log('success')
})
await queryClient.refetchQueries('current-user')
}
and one input controller
<Controller
control={control}
render={({onChange, onBlur, value}) => (
<TextField
name='name'
variant="outlined"
className={classes.input}
onChange={onChange}
onBlur={onBlur}
value={value}
/>
)}
name="name"
rules={{required: true}}
defaultValue={user.name}
/>
and the submit button
<Button onClick={handleSubmit(handleUpdateData)}>
{'save'}
</Button>
Thanks a lot :)
I do not know how your handleSubmit function looks like but this is how i handle it, assuming the *default value are from props. I'm using lodash's isEqual but you can use other object comparison functions.
const Component = (props) => {
const [ state, setState ] = useState(props);
const handleChange = (value) => {
setState({...state, field: value}) //example of state change
}
return (
..... input form
<Button disabled={isEqual(props, state)}></Button>
//using isEqual to compare props and state, if it's different, change happened.
)
}
If you have state in the same component with the input value stored in it just like this
this.state = {
inputValue : ''
};
you can use this prop inputValue to check if it's empty or not like following:
<Button {!this.state.inputValue && "disabled"} onClick={handleSubmit(handleUpdateData)}>
Save
</Button>
Or With Hooks
const [inputValue, setInputValue] = useState("");
<Button {!inputValue && "disabled"} onClick={handleSubmit(handleUpdateData)}>
Save
</Button>
I'm currently using this plugin for my react application: https://www.npmjs.com/package/react-editext.
I have multiple fields:
<EdiText
value={contact.addressLine1}
type="text"
onSave={handleSave('addressLine1')}
onCancel={(e) => setEditing(v => !v)}
inputProps={{
placeholder: 'Address Line 1',
}}
/>
<EdiText
value={contact.addressLine2}
type="text"
onSave={handleSave('addressLine2')}
onCancel={(e) => setEditing(v => !v)}
inputProps={{
placeholder: 'Address Line 2',
}}
/>
With a save handle
const handleSave = (e) => value => {
setContact({...contact, [e]: value})
};
But, I need to be able to save all fields with one button.
Now, if these were controlled form fields, I would be able to grab the value, and submit. But they're not as there is no onChange event.
Any ideas?
I didn't find in the plugin a possibility to do that. I suggest that you use a form with refs to achieve what you want.
here is an example code
import React, { useState, useRef } from "react";
import "./styles.css";
export default function App() {
const [editing, setEditing] = useState(true);
const [contact, setContact] = useState({
addressLine1: "Address 1",
addressLine2: "Address 2"
});
const [adress1, setAdress1] = useState("adress 1");
const [adress2, setAdress2] = useState("adress 2");
const form = useRef(null);
const handleSave = () => {
const adresses = {
addressLine1: form.current["adress1"].value.toString(),
addressLine2: form.current["adress2"].value.toString()
};
setContact(adresses);
console.log(contact);
};
const handleEdit = () => {
const edit = editing;
setEditing(!edit);
};
return (
<div className="App">
<form ref={form}>
<input
type="text"
value={adress1}
name="adress1"
onChange={e => setAdress1(e.target.value)}
disabled={editing}
/>
<input
type="text"
value={adress2}
name="adress2"
onChange={e => setAdress2(e.target.value)}
disabled={editing}
/>
</form>
<button onClick={handleSave}>save</button>
<button onClick={handleEdit}>edit</button>
</div>
);
}
explanation
I used state variable editing to make the fields editable or not on button edit click
I used a state variable for each field and used the react onChange function to save the value of each field when it changes.
on save button click the values of all fields states get saved to contact state
you can change the code to make it suitable for your needs. Here is a sandbox for my code:https://codesandbox.io/s/eloquent-pine-wnsw3
I'm trying to implement MVVM in React (requirement from the class I'm taking). I'm using functional components for the view and have typescript classes for the ViewModel. My components do not re-render when a property is updated in the ViewModel though.
Here's a simple example for a page that should toggle between a login and sign up form. The setCurrentForm gets called correctly and the value in the ViewModel does update, but it doesn't change the View.
// AuthView.tsx
const AuthView: React.FC = () => {
const VM = new AuthViewModel();
let form;
if (VM.currentForm === FORMS.SignUp) {
// Toggles the current form between FORMS.SignUp and FORMS.Login
form = <SignUpForm setCurrentForm={() => VM.setCurrentForm()} />
} else {
form = <LoginForm setCurrentForm={() => VM.setCurrentForm()} />
}
return (
<Container>
{/* Sign up card */}
<div className="mt-12">
{form}
</div>
</Container>
);
}
export default AuthView;
// AuthViewModel.tsx
export default class AuthViewModel {
email: string = "";
password: string = "";
currentForm: FORMS = FORMS.SignUp;
setCurrentForm() {
console.log("Setting form in VM");
if (this.currentForm === FORMS.SignUp) {
console.log("Switching to login")
this.currentForm = FORMS.Login;
} else if (this.currentForm === FORMS.Login) {
console.log("Switching to signup")
this.currentForm = FORMS.SignUp;
}
}
}
I could force the re-render with a hook by updating an arbitrary value, but I think there's a better way to do this. What are your thoughts?
You might be missunderstanding how react components re-render, just because you change some property in another object it has no bearing on the component itself, even if it has taken a property from this object.
Hooks are directly connected to the reacts render mechanism and can trigger render cycles, as such you should use something like this:
const AuthView: React.FC = () => {
// if you don't put this in a state a new VM will be created when the component rerenders
const [VM] = useState(new AuthViewModel());
useEffect(() => {
// Maybe some handler code is needed?
}, VM.currentForm);
let form;
if (VM.currentForm === FORMS.SignUp) {
// Toggles the current form between FORMS.SignUp and FORMS.Login
form = <SignUpForm setCurrentForm={() => VM.setCurrentForm()} />
} else {
form = <LoginForm setCurrentForm={() => VM.setCurrentForm()} />
}
return (
<Container>
{/* Sign up card */}
<div className="mt-12">
{form}
</div>
</Container>
);
}
export default AuthView;
I've never tried to observe a nested property via a hook, so not 100% this works.
EDIT: it doesn't work, but it makes sense, the rendering call gets triggered when you actually call the set function of the useState hook, not really sure how to implement this pattern with hooks and without something like redux or mobx, but here is my best approach:
class AuthViewModel() {
constructor(public readonly currentForm = 'LOGIN');
public setCurrentForm = () => {
if(this.currentForm === 'LOGIN')
return new AuthViewModel('SIGNUP')
else
return new AuthViewModel(); // will default to login
}
}
and then the component
const AuthView: React.FC = () => {
// if you don't put this in a state a new VM will be created when the component rerenders
const [VM, setVM] = useState(new AuthViewModel());
let form;
if (VM.currentForm === FORMS.SignUp) {
// Toggles the current form between FORMS.SignUp and FORMS.Login
form = <SignUpForm setCurrentForm={() => setVM(VM.setCurrentForm())} />
} else {
form = <LoginForm setCurrentForm={() => setVM(VM.setCurrentForm())} />
}
return (
<Container>
{/* Sign up card */}
<div className="mt-12">
{form}
</div>
</Container>
);
}
export default AuthView;
What you have here doesn't feel very React. For starters, I've only rarely seen classes used outside of class-based components. I'm just going to spitball a different solution here that might not exactly match what you need, but hopefully gets you going in a correct direction.
const Authenticate: FC = props => {
const [mode, setMode] = useState<"login" | "create">("login");
return (
<div>
{mode === "login" && <Login onLogin={({email, password}) => {/*login handler logic*/}}/>}
{mode === "create" && <CreateAccount onCreate={({email, password}) => {/*create handler logic*/}}/>}
<button
disabled={mode === "login"}
onClick={() => setMode("login")}
>
login
</button>
<button
disabled={mode === "create"}
onClick={() => setMode("create")}
>
sign up
</button>
</div>
)
}
const Login: FC<{onLogin: ({email: string, password: string}) => any}> = props => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const { onLogin } = props;
return (
<form onSubmit={() => onLogin({email, password})}>
<input value={email} onChange={e => setEmail(e.target.value)} />
<input type="password" value={password} onChange={e => setPassword(e.target.value)} />
<button type="submit">Login</button>
</form>
);
}
const CreateAccount: FC<{onCreate: ({email: string, password: string}) => any}> = props => {
return (
<div>... similar to <Login/> ... </div>
)
}
I'm not yet a React master, hence my question. Why there is still invoking a parent function if in child component I'm writing new characters in input fields? I want to call parent method only when I clicked Search button in my child component.
Parent component:
class MainPage extends Component {
render() {
let searchOffersBar = (
<MuiThemeProvider>
<SearchOffer
offersFound={this.props.onOffersFound}
/>
</MuiThemeProvider>
);
let searchResults = (
<SearchResults
offers={this.props.offers}
/>
);
return (
<Aux>
<div className={classes.container}>
<Intro/>
<div className={classes.contentSection}>
{searchOffersBar}
{searchResults}
</div>
</div>
</Aux>
)
}
}
const mapStateToProps = state => {
return {
offers: state.offers.offers
}
}
const mapDispatchToProps = dispatch => {
return {
onOffersFound: (searchParams) => dispatch(actions.fetchOffersByCriteria(searchParams))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(MainPage);
<SearchOffer> is my child component with a search section (input fields and button "Search offers"). I want to fill some data in my inputs and then click the button. I though that clicking the button will invoke a method in child component: onOffersFound:
const searchOffer = props => {
let currentDate = new Date();
const [searchCriteria, setSearchCriteria] = useState({
brand: 'xxx',
capacity: 100
})
const [drawerIsOpen, setDrawerIsOpen] = useState(false);
const handleToggle = () => setDrawerIsOpen(!drawerIsOpen);
const handleBrand = (event) => {
let mergedState = updateObject(searchCriteria, {brand: event.target.value})
setSearchCriteria(mergedState);
}
const handleCapacity = (event) => {
let mergedState = updateObject(searchCriteria, {capacity: event.target.value});
setSearchCriteria(mergedState);
}
const handleBookingFrom = (bookingFromValue) => {
let mergedState = updateObject(searchCriteria, {bookingFrom: bookingFromValue});
setSearchCriteria(mergedState);
}
const handleBookingTo = (bookingToValue) => {
let mergedState = updateObject(searchCriteria, {bookingTo: bookingToValue});
setSearchCriteria(mergedState);
}
return (
<div className={classes.sideNav}>
<Button variant={"outlined"} onClick={handleToggle} className={classes.sideNavBtn}>Search</Button>
<Drawer
className={classes.drawer}
containerStyle={{top: 55}}
docked={false}
width={200}
open={drawerIsOpen}
onRequestChange={handleToggle}
>
<AppBar title="Search"/>
<form noValidate autoComplete="off" onSubmit={props.offersFound(searchCriteria)}>
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<Grid container justify="space-around">
<TextField
id="brand"
label="Brand"
margin="normal"
onChange={handleBrand}
/>
<TextField
id="capacity"
label="Capacity"
margin="normal"
onChange={handleCapacity}
/>
<Button variant="contained" color="primary">
Search
</Button>
</Grid>
</MuiPickersUtilsProvider>
</form>
</Drawer>
</div>
);
}
export default searchOffer;
onOffersFound in my action creator looks like:
export const fetchOffersByCriteria = (searchParams) => {
return dispatch => {
let queryParams = '?brand='+searchParams.brand + '&capacity='+searchParams.capacity;
axios.get('/getFilteredOffers' + queryParams)
.then(response => {
dispatch(saveFoundOffers(response.data)); --> saves the state
})
.catch(error => {
console.log(error);
})
}
}
My question is why the above method fetchOffersByCriteria is invoked every time I enter new character in my child component? I want to invoke this method only when I click the Search button in child component. Maybe my approach is bad?
Thanks for all tips!
The issue is that props.offersFound(searchCriteria) is being invoked every render. The onSubmit prop should be a function to be invoked when submitted. Currently, it's being invoked immediately.
This line:
onSubmit={props.offersFound(searchCriteria)}
Should be (or something similar):
onSubmit={() => props.offersFound(searchCriteria)}
Currently, when typing in the brand (or capacity) field, the handleBrand change callback is invoked. This invokes setSearchCriteria (a state update) which triggers a re-render of the component. While this component is re-rendering, it's immediately invoking props.offersFound(searchCriteria) and passing the return value to the onSubmit prop. You likely want the onSubmit prop to be a function to be invoked at the time of submitting.
See the documentation for controlled components for more de3tails.
<form
noValidate
autoComplete="off"
onSubmit={props.offersFound(searchCriteria)}>
You are immediately invoking prop and trying to use result returned as event listener. It should be
<form
noValidate
autoComplete="off"
onSubmit={() => props.offersFound(searchCriteria)}>
instead