Kyle Shevlin

Software Engineer
May 02, 2023
0 strokes bestowed

Context, Composition, and Flexibility

edit

Recently, I had the following situation at work. It has been adapted to 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.


Finished reading?

Liked the post? Give the author a dopamine boost with a few "beard strokes". Click it up to 50 times show your appreciation.

Related Post:
How to Use React Context Effectively
Tags
React

Are you, or the company you work for, struggling with something mentioned in this article?
Would you benefit from a live training session?
Let's Talk
Kyle Shevlin's face, which is mostly a beard with eyes
Kyle Shevlin is a software engineer who specializes in JavaScript, TypeScript, React and frontend web development.

Let's talk some more about JavaScript, TypeScript, React, and software engineering.

I write a newsletter to share my thoughts and the projects I'm working on. I would love for you to join the conversation. You can unsubscribe at any time.

Data Structures and Algorithms Logo
Data Structures and Algorithms

Check out my courses!

Liked the post? You might like my courses, too. Click the button to view this course or go to Courses for more information.
View on egghead.io
I would like give thanks to those who have contributed fixes and updates to this blog. If you see something that needs some love, you can join them. This blog is open sourced at https://github.com/kyleshevlin/blog
alexloudenjacobwsmithbryndymentJacobMGEvanseclectic-codingjhsukgcreativeerikvorhesHaroenvmarktnoonandependabotmarcuslyonsbrentmclarkfederico-fiorinimedayzTowardsDeathFanchGadjonoahmateenbrandonpittman
©2023 Kyle Shevlin. All Rights Reserved.