React Component Composition with a UI Library
July 23, 2020
Chances are you’re using a UI library of some description, it could be Material, Reactstrap, AntD or any other number of libraries, but the question persists: how do you create a truly reusable, and pleasant to use, React Component?
Chakra UI
In the below I’ll be making use of Chakra UI, as I rather enjoy working with it and I bet you will to, if you don’t already.
I would note that, however, at the time of writing, the stable version is 0.8.0. It looks like 1.0.0 is almost ready but it’s still definitely in a “not yet” state.
What We’ll Make
Following on from my last ordeal where we created an input component, we’ll be doing the same again, though with Chakra and building it out a little further.
The Blessing and the Curse
Another quick note before diving in: the best and worst part of Chakra is how flexible it is. Consider the below:
import { Button } from '@chakra/core';
const MyComponent = () => (
<div>
<Button
color="red"
borderColor="green"
borderTopWidth="5px"
textAlign="right"
flexDir="column"
padding="2.2rem"
>
Submit
</Button>
</div>
);
With very little effort we’ve put a button in a component, and styled it with a myriad of css props it accepts. This is, clearly, amazing for rapidly prototyping, but if you used this approach throughout an entire app — and then had to refactor — you’d be in for a bad time.
Onward
We’ll start with an input component and a view to make it a password input. Our objective is to enfore certain patterns of use. Here’s the basis for what comes next:
import React from 'react';
import { Input as ChakraInput, InputProps } from '@chakra-ui/core';
export const PasswordInput = ({ children, ...rest }: InputProps) => (
<ChakraInput {...rest} type="password">
{children}
</ChakraInput>
);
Note we’re spreading all the remaining props onto the base component. I wouldn’t be comfortable doing this without TypeScript, and I’m not entirely comfortable even so.
There’s a rule in the ESLint React plugin to warn of it, too, but how you manage that is up to you.
Enforcing Props
Chakra is largely accessible out of the box, but there are a few ways this could be improved, notably with various props enforced, so how do we do that? TypeScript of course:
// know the props we want to omit from the Chakra input props
type OmittedProps = 'name' | 'id' | 'autocomplete' | 'type';
// create a new type with those omitted
type BaseProps = Omit<InputProps, OmittedProps>;
// define a type we can use for autocomplete
type AutoComplete = 'on' | 'off' | 'new-password' | 'current-password';
// define a type containing the removed props, only this time make then required
type RequiredProps = {
name: string;
id: string;
autocomplete: AutoComplete;
};
// create a type to be used as the actual component props
type Props = BaseProps & RequiredProps;
// use these props in our component
export const PasswordInput = ({ children, ...rest }: Props) => (
<ChakraInput {...rest} type="password">
{children}
</ChakraInput>
);
Just like that, we’ve made a component that requires the name, id, and autocomplete. Further, we haven’t broken the very large remainder of the Chakra input API.
On Styling
Depending on your use case, it may be preferential to have some base styles defined for the input, and not allow those to be overwritten by provided props. One way of going about it is as below:
// define an object with your base styles
const baseStyle = {
border: '1px solid black',
borderRadius: '5px',
};
export const PasswordInput = ({ children, ...rest }: Props) => (
// spread the styles alongside the rest of the props
<ChakraInput {...rest, ...baseStye} type="password">
{children}
</ChakraInput>
);
In the event yoou want base styles to be overwritten, just spread them before
the {...rest}
. Most likely all inputs will have the same base style, and I
would argue the same behaviour in terms of overriding, which is something to be
aware of.
On to the Label
The label is simpler, so we’ll skip talking and and going direct to implementation.
import React from 'react';
import { FormLabel } from '@chakra-ui/core';
import { FormLabelProps } from '@chakra-ui/core/dist/FormLabel';
// Remove optional `htmlFor` from base props, add it back as a required prop
type Props = Omit<FormLabelProps, 'htmlFor'> & { htmlFor: string; };
// have some default styles for it
const baseStyle = {
fontSize: '0.875rem',
};
// And put it all together
export const Label = ({ children, ...rest }: Props) => (
<FormLabel {...rest, ...baseStyle}>{children}</FormLabel>
);
Yes it’s simple, but the idea to drive home is that each input should be paired with a label, and they should behave per web standards, or as best as they can being in a SPA.
If you don’t normally use labels and rely on using placeholder
instead, that’s
fine, but you should still have a label and hide it in a way that doesn’t break
web accessibility.
Creating a Form Group
This isn’t strictly necessary, but if you want to really make it structured and have just one input group with generally expected and opinionated behaviours, then it’s worth considering.
import React from 'react';
import { FormControl } from '@chakra/core';
import { Label, PasswordInput } from 'components/form';
interface Props {
// The `id` will be used as `htmlFor` and `name` if they're not provided
id: string;
htmlFor?: string;
name?: string;
value: string;
// You can use the below or export the type from earlier for it (do that)
autocomplete: 'on' | 'off' | 'new-password' | 'current-password';
// If you're using hooks and have a `setPassword` in the parent component then
// this would make sense to use
onChange: (value: string) => void;
// Else if you have a class that takes in the entire even then the below
// onChange: (e: ChangeEvent) => void;
}
// Create a type, probably in another file really, for use through all inputs.
// Not strictly necessary, but can be easier on the eyes overall.
type ChangeEvent = React.ChangeEvent<HTMLInputElement>;
export const PasswordInputGroup = ({
id,
htmlFor = id,
name = id,
value,
onChange,
autocomplete,
}: Props) => (
<FormControl>
<Label htmlFor={id}>Password</Label>
<PasswordInput
name={id}
id={id}
autocomplete={autocomplete}
value={value}
onChange={(e: ChangeEvent) => onChange(e.target.value)}
/>
</FormControl>
);
With a minimum of work, we now have a form group, well, a PasswordInputGroup
.
We also reduce the net total of required props by using id
for name
and
htmlFor
if they’re undefined.
The <FormControl />
component takes props of it’s own, e.g.: isInvalid
, and
we aren’t considering those in this case. However, I imagine you can see how
easy it’d be to add that in.
Name Components Considerately
Name components based on what they do or are otherwise responsible for.
Where practical, don’t name your components the same as those you’ll be using from a UI library, as it’ll mean more room for error when relying on automatic imports.
Potentially, depending on the size and scope of your project, a monorepo might even be a not-unwise choice, but that’s a conversation for another time.