22. Give names to behaviors not interactions

December 28, 2018

I wrote this post for my newsletter, sign up here to get emails in your inbox.


This is part of a series of posts about writing component API for great developer experience.

Here’s part #1 if you missed it: Don’t create conflicting props


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 2 of an ongoing series. If you enjoyed this, there’s more where that came from 😉


Want articles like this in your inbox?
React, design systems and side projects. No spam, I promise!