Straight forward reusable React components
February 2, 2022
Building clean reusable components and avoiding decision paralysis when choosing their props.
Background
I've become a big fan of not using too many libraries in my frontend applications both because of performance concerns and often pre-made components styles are hard to override with small tweaks.
Now days
- Typically like writing straight simple CSS.
- Try not to write reusable components too early (premature optimization/abstractions) Reusable components should carry their own state
Examples
HR - Horizontal Rule
Let's take a
<hr/>
HTML tag for example. Rather than give any specific element
a bottom border we can instead use an <hr/>
reusable component.

Writing a reusable component can usually result in decision paralysis
- How do we handle all potential prop cases (further demonstrated in next component)
- If we want to override the color defined in the CSS we can pass those in as styles
By CMD clicking say the
div
tag we can see the prop types.
// HorizontalRule.tsx import React from "react"; import styles from "./HorizontalRule.module.css"; type HorizontalRuleProps = React.DetailedHTMLProps< React.HTMLAttributes<HTMLHRElement>, HTMLHRElement >; const HorizontalRule: React.FC<HorizontalRuleProps> = (props) => ( <hr className={styles.root} {...props} /> ); export default HorizontalRule;
/*HorizontalRule.module.css*/ .root { border-top: 1px solid var(--greyLight); width: 100%; }
A couple of things to note, about this design pattern.
- Basic CSS styles are applied through the classname prop
- Props are after the initial classname allowing us to pass in a custom classname later or just a styles object
Input
On the Input component a few notes
- The onChange function type is overrode and automatically return the value rather than the element
- Now we can automatically pass props to input autocomplete, required, regex patterns, etc with having to manually define them again
- Even tho we have a custom onChange method we can still override to get the original element (although we might need two on change function type declarations)
import React from "react"; import styles from "./Input.module.css"; interface Props extends Omit< React.DetailedHTMLProps< React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement >, "onChange" > { id: string; label?: string; onChange: (value: string) => void; } const Input: React.FC<Props> = ({ label, onChange, ...props }) => ( <div className={styles.root}> {label && ( <label htmlFor={props.id} className={styles.label}> {label} </label> )} <input className={styles.input} onChange={(e) => onChange(e.target.value)} {...props} /> </div> ); export default Input;
Dropdown selector
We can take this idea even further when implementing a dropdown selector in the code below we can now pass any props to the root/label/select/options tags.
These types come from the react library typings themselves but are not exported.
import React from "react"; import styles from "./DropdownSelect.module.css"; export interface DropdownSelectProps { id: string; name: string; value: string; values: Array<[value: string, text: string]>; onChange: (value: string) => void; rootDivProps?: React.DetailedHTMLProps< React.HTMLAttributes<HTMLDivElement>, HTMLDivElement >; labelProps?: React.DetailedHTMLProps< React.LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement >; selectProps?: React.DetailedHTMLProps< React.SelectHTMLAttributes<HTMLSelectElement>, HTMLSelectElement >; optionsProps?: React.DetailedHTMLProps< React.OptionHTMLAttributes<HTMLOptionElement>, HTMLOptionElement >; } const DropdownSelect: React.FC<DropdownSelectProps> = (props) => ( <div className={styles.root} {...props.rootDivProps}> <label htmlFor={props.id} {...props.labelProps}> {props.name} </label> <select name={props.name} id={props.id} onChange={(e) => props.onChange(e.target.value)} > {props.values.map(([value, text]) => ( <option key={value} value={value}> {text} </option> ))} </select> </div> ); export default DropdownSelect;
RadioButtons
import React from "react"; import styles from "./RadioButtons.module.css"; interface RadioButtonsProps { name: string; selectedValue: string; onSelect: (value: string) => void; options: Array<{ id?: string; value: string; text: string }>; rootDivProps?: React.DetailedHTMLProps< React.HTMLAttributes<HTMLDivElement>, HTMLDivElement >; labelProps?: React.DetailedHTMLProps< React.LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement >; radioButtonProps?: React.DetailedHTMLProps< React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement >; } const RadioButtons: React.FC<RadioButtonsProps> = (props) => ( <div className={styles.root} {...props.rootDivProps}> {props.options.map((option) => ( <div key={option.value}> <input className={styles.input} {...props.radioButtonProps} value={option.value} checked={option.value === props.selectedValue} onChange={({ target: { value } }) => props.onSelect(value)} id={option.id ?? option.value} type="radio" /> <label htmlFor={option.id ?? option.value} {...props.labelProps}> {option.text} </label> </div> ))} </div> ); export default RadioButtons;
.root { display: flex; flex-direction: column; gap: 0.5rem; }
Form component
- Classnames can be passed in addition the the bass styles
- onSubmit prop automatically prevents the default form action to happen
import React, { ReactNode } from "react"; import styles from "./Form.module.css"; export interface FormProps { id?: string; className?: string; header?: ReactNode; actions?: ReactNode; loading?: boolean; errors?: string[]; onSubmit?: () => void; } const Form: React.FC<FormProps> = (props) => ( <form id={props.id} className={ props.className ? `${styles.root} ${props.className}` : styles.root } onSubmit={(e) => { e.preventDefault(); props.onSubmit && props.onSubmit(); }} > {props.header} <div className={styles.errors}> {props.errors?.map((e, index) => ( <span key={index}>{e}</span> ))} </div> <div className={styles.fields}>{props.children}</div> <div className={styles.actions}> {props.actions || <button type="submit">Submit</button>} </div> </form> ); export default Form;
.root { display: flex; flex-direction: column; gap: 1rem; width: 100%; align-content: center; /*max-width: 30rem;*/ } .errors { } .fields { display: flex; flex-direction: column; gap: 1rem; } .actions { float: right; } .actions input { float: right; } .actions button { float: right; }