Higher Order Component pattern causing confusion
The Context
Working on an old React codebase (started before the introduction of hooks in version 16), using GraphQL to mediate contracts between the frontend and the backend and Redux Form to control form states.
A colleague asked for some help after he noticed a bug caused by something seemingly un-related.
The Problem
He was working to add a new toggle field to a form. The form was submitted through a GraphQL mutation. The outer form component had some logic to control whether the submit button was disabled based on whether the
formName
was defined and whether the form was “pristine” (disabled={isPristine || !formName}
).The code looked like this:
const Form = () => { ... return ( <Form> <PageHeader button={<SubmitButton ... disabled={isPristine || !formName} />} /> </Form>) } const SubmitButton = ({ disabled }) => { ... return (<Button ... disabled={disabled} />) }
When he introduced a syntax error to the GQL mutation which was used to submit the form, unexpectedly, the submit button (which did not appear to be calling or invoking the GQL mutation) remained always disabled, regardless of the values of
isPristine || !formName
.Debugging
Whilst characterising the problem, we noticed that a GQL mutation was being sent on page load (the same one we would use to submit the form!).
The first thing we did to characterise the bug was adding a
debugger;
statement in the parent component and inside the button component to check the values of formName
, isPristine
, and disabled
in the inner component.Evaluating both in the console, in the outer component,
isPristine || !formName
evaluated to false
, whereas the submit button was receiving the prop disabled
as true
!My current mental model of what was happening:
The Extra Complexity
It turns out that there was an extra wrapper for the component which connects the submit button to redux and applies some custom middleware:
import { connect } from 'react-redux'; import { compose } from 'redux'; const SubmitButtonWrapper = compose( ..., // middleware withPermissionsValidation(), connect (null, mapDispatchToProps) )(SubmitButton;
My new mental model is:
The
withPermissionsValidation
function creates and returns a new class component, which has a componentDidMount
method which calls the mutation. The idea is to invoke the mutation with no data, just to check if the user has permission to do the action.The Higher Order Component (HOC) looked like this:
class WrappedComponent extends React.PureComponent { state = { // the child component always starts with disabled=true disabled: true, } componentDidMount() { this.props.mutation() // never gets hit if GQL syntax error in mutation .then(() => this.setState({ disabled: false })) .catch() } }
If there is a generic error which doesn’t relate to permissions (for example a syntax error in GQL), the mutation will return with an error and the disabled prop to the child component is overridden!