A good api has a huge impact on the productivity of the team and stability of the product.
The secret to designing a good component api is the mix of
- splitting the components to smaller components
- passing minimal data to the components
- deduce the data from existing data instead of having a separate state.
- have similar api for similar components
- Keep it futuristic and extendable
For this post, we will group the components mainly into two categories.
- UI Primitive components
- Functional / Large UI components (Part 2)
UI Primitive components
These are the components usually we put into a component library. To make the best out of the primitive UI components, the api for these components should focus on being generic and extendable. These components are going to be used throughout the whole project in different use cases and to compose larger components.
To illustrate the good and bad, we take an example of Button
component which is a wrapper to the HTML element button but with
some of our product specific customisations.
Accept children
Consider the Button
component which has only text
prop which can accept a string. And suddenly an new scenario came up to show the text in bold
or a new icon for the text. Now this requires changes to our Button
api. we need to accept new parameters to make this possible. First thought process
for a newbie will add a new prop which can say whether to render the text in bold or add a new prop to accept icon url.
// bad
const Button = ({ text, bold, iconUrl }) => {
const t = bold ? <strong>{text}</strong> : text;
const icon = iconUrl ? <image src={iconUrl} /> : null;
return <button>{icon} {t}</button>;
}
But if we use this approach we keep on adding new props for any new formatting changes in future and end up with lots of props. This will get worse when more customisations like icon placement or other props for the icon.
The better solution will be accepting children
. By this Button
component doesn’t need to make any more changes to add
icons or any style changes to the text.
// better
const Button = ({ children }) => {
return <button>{children}</button>;
}
In this case the developer who uses the button has more control on what and how to render the content/text. if he needs icon
<Button>
<image src={iconUrl} />
<strong>Hello World</strong>
</Button>
In case we have too much usage of Button with Icon we can use Button
to compose and create new component ButtomWithIcon
for the specific usage.
use the same api as the base component
To make it easy to use our custom component, we can reuse the same api of the base component for our custom component as well. This will help our team to get used to the custom component easily since they don’t want to learn about new props. The custom component will look more familiar to them.
// bad
const Button = ({ children, handleSubmit, enabled }) => {
return <button disabled={!enabled} onClick={handleSubmit}>{children}</button>;
}
In the above case, the team has to go and check the documentation or the component to figure out which prop to be used. The above api can make the team more confused and difficult to use the component.
The better approach will be
// better
const Button = ({children, disabled, onClick}) => {
return (
<button disabled={disabled} onClick={onClick}>
{children}
</button>
);
}
Once we start following the above approach we can extend that into next tip.
Accept valid base component props
Accepting the props which are required only for basic scenarios won’t be enough. We can avoid frequent addition of props when the new requirement arises. using the Rest/Spread.
// bad
const Button = ({ children }) => {
return <button>{children}</button>;
}
The new api using rest
will look like
// good
const Button = ({ children, ...rest }) => {
return <button {...rest}>{children}</button>;
}
Avoid unnecessary computation
The primitive UI components should not contain any kind of business logic. Including business logic makes it difficult to use them as generic components.
// bad
const Button = ({ children, name }) => {
const disabled = name === "";
return <button disabled={disabled}>{children}</button>;
}
The above one has a condition to disable the button when the name is empty. But this won’t be the requirement always.
Button
component doesn’t need to know on what basis it should render disabled
. The condition can be different for each requirement.
// better
const Button = ({ children, disabled, onClick }) => {
return <button disabled={disabled} onClick={onClick}>{children}</button>;
}
<Button disabled={name === ""} />
We will cover the Functional / Large UI components
in the next post.