May 02, 2023

Context, Composition, and Flexibility

edit

Recently, I had the following situation at work. It has been adapted to be less professional and more humorous:


Designer: Hey, you know our Input component?

Me: Yeah, this one?

Designer: Yeah, that’s the one! Look, I want you to make it so that when the Input is optional, it shows a little bit of helper text to the right of label that says "(optional)". Like this:

Me: Oh, you wouldn’t prefer to show an asterisk when it’s required like almost every other website in existence? Like this:

Designer: Well, most of our Inputs should be required so we felt that this would be less noisy. Don’t want to see *s everywhere.

Me: But every Input in all our apps, and all the web actually, is optional by default. You have to set the required attribute explicitly. Meaning the more likely scenario is seeing "(optional)" everywhere.

Designer: Yeah… I don’t care. Just do it.


Now, if you’ve done software engineering for any significant amount of time, you’ve probably run into a scenario just like this one. It sucks to be told to do something that you know isn’t the right solution.

That said, in this particular scenario, there’s a pretty easy way for both of us to win and I’m hoping it will give you some more strategies when you’re in a similar situation again in the future. Let me show you how.

First instincts vs. second thoughts

Given the nature of the change, it would be really tempting to solve it as simply as possible, perhaps like this:

function Input({ id, label, required = false }) {
  return (
    <div>
      <label htmlFor={id}>
        {label}
        {!required && ' (optional)'}
      </label>
      <input id={id} required={required} />
    </div>
  )
}

This definitely solves the problem, but we know that this isn’t a great solution. It’s likely to get push back from other teams who want something they’re more accustomed to, like an * on required fields.

We might be tempted to change it so that Input receives a variant that determines how to handle this, perhaps like this:

function Input({ id, label, required = false, variant = 'showOptionals' }) {
  const getHelperText = () => {
    if (variant === 'showOptionals' && !required) return ' (optional)'
    if (variant === 'showRequireds' && required) return '*'

    return null
  }

  return (
    <div>
      <label htmlFor={id}>
        {label}
        {getHelperText()}
      </label>
      <input id={id} required={required} />
    </div>
  )
}

Now, if a team wants to show a more traditional * on their required inputs, they can add the variant="showRequired" prop. However, this would mean needing to add that to every single Input that needs that treatment. That’s laborious, exhausting and tedious.

What if there was a way to apply a variant to every input, but only have to write it once?

Context and composition to the rescue

Let’s create an InputStyleContext that will pass the variant to all consumers automatically.

const InputStyleContext = React.createContext()

function InputStyleProvider({ children, variant = 'none' }) {
  return (
    <InputStyleContext.Provider value={variant}>
      {children}
    </InputStyleContext.Provider>
  )
}

const useInputStyleContext = () => React.useContext(InputStyleContext)

Now that we have that, let’s use the custom hook we made to get the variant off of context, rather than passed in as a prop to Input.

function Input({ id, label, required = false }) {
  const variant = useInputStyleContext()

  const getHelperText = () => {
    if (variant === 'showOptionals' && !required) return ' (optional)'
    if (variant === 'showRequireds' && required) return '*'

    return null
  }

  return (
    <div>
      <label htmlFor={id}>
        {label}
        {getHelperText()}
      </label>
      <input id={id} required={required} />
    </div>
  )
}

Now we have the option to change the appearance of our Inputs with a single wrapping component. You want the "(optional)" text, do this:

<InputStyleProvider variant="showOptionals">
  <Input id="name" label="Name" />
</InputStyleProvider>

Or if you want to show * on required fields, change the variant to showRequireds. It’s that simple.

<InputStyleProvider variant="showRequireds">
  <Input id="name" label="Name" required />
</InputStyleProvider>

Now we have a solution that has satiated our designer, but gives us some flexibility still. I think that’s a win-win.

Try to remember that context and composition might be a good solution for your problem, too.

And for the record…

The very first form I worked on after this change was, in fact, 5 Inputs, all optional, all with the "(optional)" text next to them. So much for being less noisy! Oh well.


Liked the post?
Give the author a dopamine boost with a few "beard strokes". Click the beard up to 50 times to show your appreciation.
Kyle Shevlin's face, which is mostly a beard with eyes

Kyle Shevlin is the founder & lead software engineer of Agathist, a software development firm with a mission to build good software with good people.

Logo for Introduction to State Machines and XState
Introduction to State Machines and XState
Check out my courses!
If you enjoy my posts, you might enjoy my courses, too. Click the button to view the course or go to Courses for more information.
Sign up for my newsletter
Let's chat some more about TypeScript, React, and frontend web development. Unsubscribe at any time.