DEV Community

Sid
Sid

Posted on

Give names to behaviors not interactions

When it comes to React components, props are the API that developers consume. A good API should be obvious, something the developer can guess. You want to make it easier for the developer to implement their feature and move on.

This is valid not just for developers creating component libraries, but also for developers building applications. Your team mates have to use the component API you create.

After consuming a bunch of articles + talks and doing an inventory of all the props we have in cosmos, I've come up a few guidelines.

Here's one of them:

This post was originally posted on my newsletter a few weeks ago, just saying.

We have this Switch component that accepts a prop, let's call it something for now.

A developer using our component can pass a function and we'll call it when the value changes.

switch

<Switch something={fn} />

React gives us the freedom to call the prop whatever we want - handler / clickHandler / onClick / onToggle etc.

It has become sort of a popular convention to start your event handlers with an 'on' like onClick. This is because the HTML spec has a bunch of handlers that follow this convention already: onkeydown, onchange, onclick, etc.

Reusing an already existing convention is a great idea, your developers don't have to learn a new thing.

Okay, how about onClick?

<Switch onClick={fn} />

I'm not a big fan of the onClick handler here because it assumes that a mouse click is the only way to interact with this component.

Users on a mobile device would tap the switch with their finger or drag it to the right. Users with visual impairment will use it with a screen reader and keyboard keyPress.

As a developer using this component, I don't want to think about how end users interact with this component. I just want to attach a function that is called when the value changes.

Let's use a interaction agnostic API:

<Switch onToggle={fn} />

That makes sense, right? The switch toggles between it's two values.

Inside the component, you might want to proxy all possible interactions to the same function

function Switch(props) {
  return (
    <div
      className="switch"
      /* click for mouse users */
      onClick={props.onToggle}
      onKeyDown={function(event) {
        /* if the enter key is hit, call event handler */
        if (event.key === 'Enter') props.onToggle(event)
      }}
      onDrag={function(event) {
        /* pseudo code */
        if (event.toElement === rightSide) props.onToggle(event)
      }}
    />
  )
}

We've internalised all the implementation detail to expose a nice API for our users (developers).

Now, let's talk about a component hopefully all of us can agree on - a text input.

input

<TextInput />

HTML has an onchange attribute, the React docs use onChange in their examples as well. There seems to be consensus around this.

<TextInput onChange={fn} />

Easy peasy.

Now, let's put both these components together.

together

<TextInput onChange={fn} />
<Switch    onToggle={fn} />

Notice something odd?

Even though both the components need similar behavior, the prop is named differently. The props are perfect for their respective component, but when you look at all your components together, it's very inconsistent.

What this means for developer experience is that you always have to check what the prop is called before using it. Not ideal.

So, here's tip #2 for you: Aim for consistent props across components. The same behaviour should have the same prop across components.

This tip can also be phrased as Aim for a minimal API surface area. You should limit the amount of API a developer has to learn before they can start being productive.

That's a beautiful way to put it, all credit goes to Sebastian MarkbΓ₯ge. (I've linked his talk at the end of this post)

The way to implement this tip is to pick one prop and use it across all your components. From the two props we have in our example onChange is also in the HTML spec, so some developers might have already heard of it.

together

<TextInput onChange={fn} />
<Switch    onChange={fn} />
<Select    onChange={fn} />
// etc.

The consistency across components and the resulting ease of learning your API outweighs having the perfect prop for an individual component.


Made it till here? Great! Here's some bonus content for you.

Let's talk about that function signature for a minute.

<TextInput onChange={fn} />

An onChange event handler (fn in the above example), receives one argument - event.

It is triggered on each change to the input. You can get a bunch of useful information from this event

function fn(event) {
  console.log(event.target) // input element
  console.log(event.target.value) // text inside the input element
  console.log(event.which) // which keyboard key was hit
}

I assume most developers would be interested in event.target.value, so that they can use it for some other task - setting in state, submitting a form, etc.

In the case of our custom Switch component, every action exposes a different event. This event will have different properties for a click event and a drag event. How we make sure the API is consistent?

We can manually set event.target.value for every event:

function Switch(props) {
  /* custom handler */
  const fireHandler = event => {
    const newValue = !oldValue

    /* consistent property that devs can rely on: */
    event.target.value = newValue

    /* fire the handler from props */
    props.onChange(event)
  }

  return (
    <div
      className="switch"
      /* click for mouse users */
      onClick={fireHandler}
      onKeyDown={function(event) {
        if (event.key === 'Enter') fireHandler(event)
      }}
      onDrag={function(event) {
        if (event.toElement === rightSide) fireHandler(event)
      }}
    />
  )
}

Watch Sebastian's talk if you want to learn more about this concept: Minimal API Surface Area

Hope this was helpful on your journey

Sid

That was part of an ongoing series. If you enjoyed this, there's more where that came from πŸ˜‰

newsletter

Top comments (5)

Collapse
 
dance2die profile image
Sung M. Kim

Thank you, Sid. It was very helpful πŸ‘‹.

How I understood was that, one should specify "what" (declarative) a component should do not "how" (imperative) it should do.

So in the Switch example, onClick specifies "how" a user should interact with it, while onChange exposes a general behavior of "what" the component can do.

Would you correct my understanding as I could be getting the whole abstraction wrong πŸ˜…

Collapse
 
siddharthkp profile image
Sid

That's the perfect way of looking at it!

Collapse
 
dance2die profile image
Sung M. Kim

πŸ˜„πŸ‘Š

Collapse
 
enieber profile image
Enieber Cunha

Very good your post. How do you see use this pattern to react-native?

Collapse
 
siddharthkp profile image
Sid

I don't use react-native but I'd say the ideas still make sense .The implementation details might change, but the API would look the same