Give names to behaviors not interactions
Sid
Posted on January 3, 2019
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 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.
<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.
<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.
<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 😉
Posted on January 3, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 30, 2024