This post explores several common ways of handling visibility of conditional fields. Our sample form will have the following schema:
foo
: always presentbar
: dependent on form state (value of foo)baz
: dependent on other application state (e.g. user permissions)
Below is our form, prior to implementing visibility logic:
import React from 'react';
export default class MyForm extends Component {
render() {
const { formValues } = this.props;
return (
<form>
<input name="foo" value={formValues.foo} />
<input name="bar" value={formValues.bar} />
<input name="baz" value={formValues.baz} />
</form>
);
}
}
Examples below assume the following:
- We are using Redux to manage application state.
- Form state is stored in Redux (with something like Redux Form).
These are reasonable assumptions of any reasonably complex React application; however, the ideas below can apply even with other setups.
We store field visibility in local component state. Whenever a condition that a form field depends on changes, we update that state.
import React from 'react';
export default class MyForm extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ barVisible: props.formValues.foo === 'yes',
+ bazVisible: props.user.permissions.canSeeBar,
+ }
+ }
+
+ componentWillReceiveProps(nextProps) {
+ const { formValues, user } = this.props;
+
+ if (formValues.foo !== nextProps.formValues.foo) {
+ this.setState({ barVisible: nextProps.formValues.foo === 'yes' });
+ }
+
+ if (user.permissions.canSeeBar !== nextProps.user.permissions.canSeeBar) {
+ this.setState({ bazVisible: nextProps.user.permissions.canSeeBar });
+ }
+ }
+
render() {
const { formValues } = this.props;
+ const { barVisible, bazVisible } = this.state;
return (
<form>
<input name="foo" value={formValues.foo} />
- <input name="bar" value={formValues.bar} />
+ {barVisible && <input name="bar" value={formValues.bar} />}
- <input name="baz" value={formValues.baz} />
+ {bazVisible && <input name="baz" value={formValues.baz} />}
</form>
);
}
}
From my experiences, this tends to be the first solution people new to React reach for due to its conceptual simplicity. However, this approach scales poorly: because the developer is responsible for imperatively managing field visibility, as the number of fields and interactions grows, so does the possibility of error and state synchronization issues. Additionally, this approach tends to lead to giant, unmaintainable componentWillReceiveProps
functions.
Manual management of visibility can be avoided by passing conditionals directly to the form fields. Upon closer examination, our fieldVisibility
state actually doesn't provide us with any extra information whatsoever: any information we get from that object can be derived directly from props, 100% of the time. Thus, it doesn't make sense to store this information at all.
import React from 'react';
export default class MyForm extends Component {
render() {
- const { formValues } = this.props;
+ const { formValues, user } = this.props;
+
+ const barVisible = formValues.foo === 'yes';
+ const bazVisible = user.permissions.canSeeBar;
return (
<form>
<input name="foo" value={formValues.foo} />
- <input name="bar" value={formValues.bar} />
+ {barVisible && <input name="bar" value={formValues.bar} />}
- <input name="baz" value={formValues.baz} />
+ {bazVisible && <input name="baz" value={formValues.baz} />}
</form>
);
}
}
This solution is more declarative and requires less code, and is perfectly sufficient for many use cases. However, one shortcoming is that field visibility is entirely held within the form component: components outside the form that need access to field visibility state cannot get it.
As an example, recently we have been exploring a "summary view" component that shows progress through the form to the user. With the above approach, the summary component must be a child of the form, because there is no way to communicate this state back "upwards."
Note: the following example uses Redux. However, the general idea of lifting state up and providing data accessors can apply even without Redux.
Once we have external components that are interested in field visibility state, it makes sense to lift this state out of the form.
import React from 'react';
+import { connect } from 'react-redux';
+// formSelectors.js
+exportgetBarVisible = state => state.formValues.bar === 'yes';
+getBazVisible = state => state.user.permissions.canSeeBaz;
+myFormFieldVisibility = state => ({
+ getBarVisible: getBarVisible(state),
+ getBazVisible: getBazVisible(state),
+});
+@connect(state => ({ fieldVisibility: myFormFieldVisibility(state) }))
export default class MyForm extends Component {
render() {
- const { formValues } = this.props;
+ const { formValues, fieldVisibility } = this.props;
return (
<form>
<input name="foo" value={formValues.foo} />
- <input name="bar" value={formValues.bar} />
+ {fieldVisibility.barVisible && <input name="bar" value={formValues.bar} />}
- <input name="baz" value={formValues.baz} />
+ {fieldVisibility.bazVisible && <input name="baz" value={formValues.baz} />}
</form>
);
}
}
With this approach, we gain several powerful new capabilities:
- If fields
bar
andbaz
occur in other places in the applications and have the same display rules, the selectors defined above can be reused. - External components such as the summary view component can directly reuse the
myFormFieldVisibility
selector to determine what fields are currently visible. - Even if
<MyForm/>
is the only consumer of the field visibility selector, this method achieves separation of concerns:render()
is now only concerned with if a field should be rendered, and not about the conditions under which rendering occurs. This moves<MyForm/>
closer to being a pure display component, making it easier to reason about.
Note:
fieldVisibility
as coded above will cause unnecessary rerenders because themyFormFieldVisibility
selector returns a new object every time. This can be avoided with a cached selector library such as Reselect. This example uses vanilla Javascript just for example purposes.
Bonus: hidden
prop
Conditional logic in JSX can be ugly and hard to read, due to mixing paradigms of logic and markup. One way to avoid this is to defined a hidden
property:
// old
{condition && <Foo/>}
// new
<Foo hidden={!condition} />
We can define an <Input/>
component that is a shallow wrapper of the HTML <input/>
that adds this behavior. However, because this is the sort of functionality that we want on all form controls, it makes sense to keep this logic in a decorator:
const Hidable = Component => ({ hidden, ...props }) => !!hidden || <Component {...props}/>;
With this, we can clean up our last example:
+const DecoratedInput = Hidable(props => <input {...props}/>);
@connect(state => ({ fieldVisibility: myFormFieldVisibility(state) }))
export default class MyForm extends Component {
render() {
const { formValues, fieldVisibility } = this.props;
return (
<form>
<input name="foo" value={formValues.foo} />
- {fieldVisibility.barVisible && <input name="bar" value={formValues.bar} />}
+ <DecoratedInput name="bar" value={formValues.bar} hidden={!fieldVisibility.barVisible}/>
- {fieldVisibility.bazVisible && <input name="baz" value={formValues.baz} />}
+ <DecoratedInput name="baz" value={formValues.baz} hidden={!fieldVisibility.bazVisible}/>
</form>
);
}
}
This can be specialized further into a FormControl
decorator that applies common functionality to any control, such as providing a <label/>
, providing a tooltip, or allowing size-controlling props.