Practical advice for writing React Components
Tags: react, tailwindcss, webdev, beginners
Published: 2024-06-01 00:05:42

I've been spending a _lot_ of time with React lately - and I've developed some opinions. Disagree with any of them? Ping me at [@greveJoe](https://twitter.com/greveJoe/) on the social media platform formerly known as Twitter.

Let's get into it.

## 1. Combine Classes with clsx and twMerge

Blind concatenation of classNames is a trap, especially with Tailwind. Half the time it won't work how you expect because of CSS specificity. Instead, using `clsx` (or similar) and `twMerge` together provides flexibility when combining classes - letting you safely override default component styles with your own.

### Why This Matters

- **Cleaner Code:** `clsx` and `twMerge` help you write concise and readable class combinations.
- **Conditional Classes:** `clsx` makes it easy to apply classes based on props or state.
- **Conflict Resolution:** `twMerge` allows you to override conflicting classes, ensuring the desired styles are applied.

### Example: Custom Button Component

❌ **Don't do this**:

```jsx
const MyButton = ({ isActive, children }) => (
  <button className={`bg-blue-500 text-white px-4 py-2 ${isActive ? 'border-2 border-red-500' : ''}`}>
    {children}
  </button>
);
```

*Why this is bad*: Concatenating classes directly can lead to hard-to-read and error-prone code, especially as the number of conditional classes increases.

✅ **Do this instead**:

```jsx
import clsx from 'clsx';
import { twMerge } from 'tailwind-merge';

const cn = (...classes) => twMerge(clsx(...classes));

const MyButton = ({ isActive, children }) => (
  <button className={cn(
    'bg-blue-500 text-white px-4 py-2',
    isActive && 'border-2 border-red-500'
  )}>
    {children}
  </button>
);
```

### Creating a Utility Function

To make combining classes even more convenient, create a utility function that combines `clsx` and `twMerge`:

```jsx
import clsx from 'clsx';
import { twMerge } from 'tailwind-merge';

const cn = (...classes) => twMerge(clsx(...classes));

export default cn;
```

You can then import and use this `cn` function in your components:

```jsx
import cn from './utils/cn';

const MyComponent = ({ isActive }) => (
  <div className={cn('bg-white p-4', isActive && 'border-2 border-blue-500')}>
    {/* ... */}
  </div>
);
```

By using `clsx` and `twMerge` together, you can create cleaner, more maintainable code when working with utility-first CSS frameworks like Tailwind CSS. This approach makes it easier to apply conditional classes, resolve conflicts, and keep your components readable as your application grows in complexity.

## 2. Embrace Default Props

When building reusable components, it's crucial to allow for default props like `className` or `onClick`. This practice makes your components more composable, flexible, and easier to integrate into different contexts. It ensures that other developers can use your custom components just like native HTML elements, without needing to learn their inner workings.

In React, `forwardRef` is a powerful tool that allows your custom components to behave like native HTML elements. It enables your component to receive a `ref` prop, which can be used to access the underlying DOM element directly.

### Why This Matters

Allowing default props:
- **Enhances Flexibility:** Developers can style your component or attach event handlers without modifying its internals.
- **Improves Composability:** Your component can be seamlessly used within different layouts and contexts.
- **Reduces Learning Curve:** Other developers can use familiar props, making your component easier to adopt.

### Example: Custom Button Component

❌ **Don't do this**:
```jsx
const MyButton = ({ children }) => (
  <button className="bg-blue-500 text-white px-4 py-2">
    {children}
  </button>
);
```
*Why this is bad*: This approach hardcodes styles and doesn't allow for additional props like `className` or `onClick`, making the component inflexible and hard to reuse.

✅ **Do this instead**:
```jsx
import React, { forwardRef } from 'react';
import clsx from 'clsx';
import { twMerge } from 'tailwind-merge';

const cn = (...classes) => twMerge(clsx(...classes));

const MyButton = forwardRef(({ children, className, ...props }, ref) => (
  <button 
    ref={ref}
    className={cn('bg-blue-500 text-white px-4 py-2', className)}
    {...props}
  >
    {children}
  </button>
));

export default MyButton;
```

### Using the Custom Button

Here's how you might use this `MyButton` component in a parent component:

```jsx
import React, { useRef } from 'react';
import MyButton from './MyButton';

const ParentComponent = () => {
  const buttonRef = useRef(null);

  const handleClick = () => {
    if (buttonRef.current) {
      console.log('Button clicked!', buttonRef.current);
    }
  };

  return (
    <div>
      <MyButton ref={buttonRef} className="extra-class" onClick={handleClick}>
        Click Me
      </MyButton>
    </div>
  );
};

export default ParentComponent;
```

By embracing default props and using `forwardRef`, you create components that are flexible, reusable, and behave like native HTML elements. This approach reduces the learning curve for other developers and makes your components easier to work with in various contexts.

Here are points 3-5 and the conclusion of the blog post:

## 3. Let Parents Handle Layout and Spacing

When building reusable components, it's important to let the parent component handle layout and spacing concerns. This separates the responsibilities of the parent and child components, promoting a more modular and reusable design. A more controversial way to phrase this is: Never use `margin` - `padding` and `gap` are all you need.

### Why This Matters

- **Modularity:** Keeping layout and spacing concerns in the parent component makes the child components more modular and reusable.
- **Flexibility:** Parent components can adapt the layout and spacing based on their specific needs.
- **Consistency:** Centralizing layout and spacing decisions in the parent component ensures a consistent user interface.

### Example: List Component

❌ **Don't do this**:

```jsx
const ListItem = ({ children }) => (
  <div className="mb-4">
    {children}
  </div>
);

const List = () => (
  <div>
    <ListItem>Item 1</ListItem>
    <ListItem>Item 2</ListItem>
    <ListItem>Item 3</ListItem>
  </div>
);
```

_Why this is bad_: Each `ListItem` has a hard-coded `margin-bottom`, which can lead to inconsistent spacing and make the component less adaptable.

✅ **Do this instead**:

```jsx
const ListItem = ({ children }) => (
  <div>{children}</div>
);

const List = () => (
  <div className="flex flex-col gap-4">
    <ListItem>Item 1</ListItem>
    <ListItem>Item 2</ListItem>
    <ListItem>Item 3</ListItem>
  </div>
);
```

By letting the parent `List` component handle the spacing between `ListItem`s, you create a more modular and flexible design that can easily adapt to different layout requirements.

## 4. Use Controlled Components for Complex Interactions

For complex interactions like forms or modals, use controlled components to manage state and provide a clear way for the parent component to control the child component's behavior.

### Why This Matters

- **Predictability:** Controlled components make the state and behavior of the child component more predictable.
- **Easier Testing:** With controlled components, it's easier to write unit tests for the parent component.
- **Better Separation of Concerns:** The parent component manages the state, while the child component focuses on rendering and user interactions.

### Example: Modal Component

```jsx
const Modal = ({ isOpen, onClose, children }) => (
  <div className={cn('fixed z-10 inset-0 overflow-y-auto', isOpen ? 'block' : 'hidden')}>
    <div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
      {/* Modal content */}
      <div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
        {children}
        <button onClick={onClose}>Close</button>
      </div>
    </div>
  </div>
);
```

In this example, the `Modal` component is controlled by the parent component through the `isOpen` and `onClose` props. This makes the modal's state predictable and easier to manage.

## 5. Automatically Order Tailwind Classes with Prettier

To make your Tailwind classes more consistent and easier to read, use the `prettier-plugin-tailwindcss` plugin to automatically order your classes.

### Why This Matters

- **Consistency:** Automatically ordering your Tailwind classes ensures a consistent style throughout your codebase.
- **Readability:** Ordered classes are easier to scan and understand at a glance.
- **Maintainability:** Consistently ordered classes make it easier for other developers to work with your code.

### Example: Setting Up Prettier with Tailwind CSS

1. Install the necessary packages:

```bash
npm install -D prettier prettier-plugin-tailwindcss
```

2. Update your `.prettierrc` file:

```json
{
  "plugins": ["prettier-plugin-tailwindcss"]
}
```

Now, whenever you run Prettier on your codebase, your Tailwind classes will be automatically ordered, improving the consistency and readability of your code.

## Conclusion

Remember, these practices are not hard-and-fast rules but rather guidelines based on my experience. Feel free to adapt them to your specific needs and preferences.

If you have any other tips or best practices for building React components, I'd love to hear them! Connect with me on Twitter at [@greveJoe](https://twitter.com/greveJoe) and let's continue the conversation.

Happy coding!