I’m learning react and I am stuck. I am trying to share a form between Edit and Create components. I am having a really hard time figuring out what I am doing wrong to initialize the values of the checked property for the radio group. After the page loads I can toggle the radio buttons. Initially, however, neither is checked.
I’m starting to think I am setting up my checked property incorrectly on the form.
Edit component (forms parent component) :
class CompanyMasterEdit extends React.Component {
componentDidMount() {this.props.fetchCompany(this.props.match.params.companyId,this.props.match.params.companyName)}
onSubmit = (formValues) => { this.props.editCompany(this.props.match.params.companyId, formValues) }
render() {
return (
<div style={{ padding: 50 }}>
<h1 style={{ textAlign: "center" }}>Edit Company Master Record</h1>
<h5 style={{ textAlign: "center" }}>
<span style={{ color: "red" }}>*</span> indicates required field
</h5>
<CompanyMasterForm
initialValues={_.pick(
this.props.company,
"isKeyCompany",...
)}
onSubmit={this.onSubmit}
/>
const mapStateToProps = (state, ownProps) => {
return { company: state.companies[ownProps.match.params.companyId] }
}
export default connect(mapStateToProps, { fetchCompany, editCompany })( CompanyMasterEdit)
Form:
class CompanyMasterForm extends React.Component {
<form
className="ui form error"
onSubmit={this.props.handleSubmit(this.onSubmit)}
>
<Field
label="Is This a Key Company?"
component={RadioGroup}
name="isKeyCompany"
required={true}
options={[
{ title: "Yes", value: true },
{ title: "No", value: false },
]}
checked={this.props.initialValues.isKeyCompany}
// onClick={this.setIsKeyCompany}
//onClick={(e) => this.setState({ isKeyCompany: e.target.checked })}
onClick={() => {this.setState((prevState) => {return {checked: !this.props.initialValues.isKeyCompany,}})
}}
/>
<button className="ui button primary">Submit</button>
</form>
RadioButtonGroup as a separate component:
class RadioGroup extends React.Component {
render() {
return (
<div className={className}>
<div className="inline fields">
<div className={labelClassName}>
<label>{label}</label>
</div>
{options.map((o) => {
const isChecked = o.value ? "true" : "false"
console.log("o.value :", o.value)
return (
<label key={o.title}>
<input
type="radio"
{...input}
value={o.value}
checked={isChecked === input.value}
/>
{o.title}
</label>
)
})}
</div>
</div>
)
}
}
// const mapStateToProps = (state) => {
// console.log("radio group state : ", state)
// return { isKeyCompany: state.form.companyMasterForm.values.isKeyCompany }
// }
// export default connect(mapStateToProps)(RadioGroup)
// export default connect(null)(RadioGroup)
export default RadioGroup
I have a codesandbox up. I feel like I’m close and dancing all around it but I can’t get it. Any help is appreciated.
1
3 Answers
A couple of remarks:
- Radio buttons that are mutually exclusive should have the same
name
prop. So we can just write a hardcoded string for one set of options. - The
value
prop doesn’t determine whether the radio button is active. This is done viachecked
/defaultChecked
instead. Thevalue
prop is particularly useful if we have more than two options (to label each option). - It’s not clear what the variable
input
is doing in the last code example? - It’s always better to avoid
"true"
and"false"
as string values, if there are only two possible values. In this case, a boolean (true
orfalse
without the quotation marks) is more suitable. This is the simplest way to represent your state, and typos such as"True"
won’t slip through the cracks (when you’re using strings, such typos will go unnoticed). - If you’re using uncontrolled components (i.e. the value of an input is not kept in the component’s state), you should use
defaultChecked
instead ofchecked
, if you want the user to be able to toggle the checkbox after the page has loaded. - If you want to reuse the state of the checkbox elsewhere in your app, you’ll have to use controlled components. In this case, setting the state should be done slightly differently as well (see below).
Edit: some additional remarks in response to your comment below.
(The code you provided contains syntax errors as-is, it’s easier to provide help if the code is correct and as minimal as possible (and nicely formatted). 😊)
- You shouldn’t set your state
onSubmit
(on the form element), as you seem to be doing here. The changes should be setonChange
(on the input element), because the state of the input element is the corresponding state of your app, so they must always be in sync. - The function that actually seems to set your state is the
editCompany
reducer. So you should make it available to theinput
element, either by passing it down from a parent component (ugly) or by connecting the input component to the Redux store (cleaner).
class CompanyMasterEdit extends React.Component {
componentDidMount() {
const { fetchCompany, match } = this.props;
fetchCompany(match.params.companyId, match.params.companyName);
}
render() {
const { company } = this.props;
return (
<div style={{ padding: 50 }}>
<h1 style={{ textAlign: "center" }}>Edit Company Master Record</h1>
<h5 style={{ textAlign: "center" }}>
<span style={{ color: "red" }}>*</span> indicates required field
</h5>
<CompanyMasterForm initialValues={_.pick(company, "isKeyCompany")} />
</div>
);
}
}
const mapStateToProps = (state, ownProps) => {
return { company: state.companies[ownProps.match.params.companyId] };
};
export default connect(mapStateToProps, { fetchCompany, editCompany })(
CompanyMasterEdit
);
class CompanyMasterForm extends React.Component {
render() {
const { initialValues } = this.props;
return (
<form className="ui form error">
<Field
label="Is This a Key Company?"
component={RadioGroup}
name="isKeyCompany"
required={true}
options={[
{ title: "Yes", value: true },
{ title: "No", value: false },
]}
checked={initialValues.isKeyCompany}
// onClick={this.setIsKeyCompany}
//onClick={(e) => this.setState({ isKeyCompany: e.target.checked })}
onClick={() => {
this.setState((prevState) => {
return { checked: !initialValues.isKeyCompany };
});
}}
/>
<button className="ui button primary">Submit</button>
</form>
);
}
}
class RadioGroup extends React.Component {
render() {
const { company, match } = this.props;
return (
<div className={className}>
<div className="inline fields">
<div className={labelClassName}>
{
label /* note: not wrapped in `<label>...</label>`, because it’s not the description of one option, but rather of the entire set. */
}
</div>
{options.map((o) => (
<label key={o.title}>
<input
type="radio"
name="isKeyCompany" // for example
checked={company.isKeyCompany === o.value} // or passed down via props
onChange={() =>
editCompany(match.params.companyId, {
...company,
isKeyCompany: o.value,
})
}
/>
{o.title}
</label>
))}
</div>
</div>
);
}
}
const mapStateToProps = (state, ownProps) => {
return { company: state.companies[ownProps.match.params.companyId] };
};
export default connect(mapStateToProps, { editCompany })(RadioGroup);
2
“If you want to reuse the state of the checkbox elsewhere in your app”… this is my only goal in life right now. I thought I conveyed that in my question, but maybe not clearly. Edit component is the Forms parent. Form component is the RBG(RadioButtonGroup) parent. My understanding was the Field component on the Form sends the RBG props. These include the onChange, onClick, or whatever other callback may need to be used. Therefore, I was trying to set all the event handlers on the Field component on the Form to pass to RBG. When RBG clicks onChange, it’s invoking the onChange passed in
–I added some more comments and updated the code example (mostly the
RadioGroup
component)– Bart
The value of an option is text (input.value is text but o.value is boolean). The checked property is boolean. ===
is never going to be quite right unless you fix one of those things.
This change makes an option show up. Not that it’s truly the right change. We probably should have a slightly more robust compare for the checked property.
checked={o.value == input.value}
It’s currently boolean because you set it here
options={[
{ title: "Yes", value: true },
{ title: "No", value: false },
]}
1
Yes. I had changed that earlier. Thus the const isChecked = o.value ? “true” : “false”. But not a good way to do it. Just running out of ideas
–
+1 on the research from @Nikki9696, but the better solution is to leave your comparison using the strict ===
check and change the options config to have string values:
options={[
{ title: "Yes", value: 'true' },
{ title: "No", value: 'false' },
]}
that’s because "true" != true
(the string value “true” is not equal to the boolean value true
, even with type-coercion). The same can be said for a comparison between "false"
and false
.
1
Yea this got pointed out along the way. That’s why I have the const isChecked = o.value ? “true” : “false”. Granted, not the best solution as @Bart points out.
–
I left an answer below. I would also recommend you study functional components. While class-based components have not been deprecated, some of the lifecycle methods in your code have been. Also, anecdotally I can tell you everything is moving towards functional components and hooks in practice.