Editor’s note: This article was last updated on 23 May 2023 by Rahul Chhodde to provide more information on propTypes and defaultProps, as well as a comparison of functional and class React components.
Tailwind CSS allows frontend developers to effortlessly transform design ideas into functional prototypes. It becomes even more delightful in componentized systems like React, where you can style the UI without having to write any CSS or CSS-in-JS. This allows you to fully concentrate on the functionality and reliability of your application.
This article highlights some essential tips for creating reusable React components with Tailwind CSS, while also following best practices for component creation.
Jump ahead:
Tailwind CSS is a utility-first CSS framework that enables you to apply CSS properties directly to your markup in a granular manner. It allows you to compose styles by combining multiple utility classes.
Utility-first frameworks like Tailwind CSS take a more flexible approach compared to UI-first frameworks, which primarily rely on providing ready-made components.
Utility CSS classes are standalone classes focused on a specific property or feature, which apply a particular styling detail individually to a given element.
The limitation of a comprehensive UI framework is that it often lacks the flexibility to customize existing components according to your specific project requirements. These frameworks or libraries tend to prioritize a specific UI style rather than catering to the unique needs of your project.
If you often rely on important!
CSS declarations as a workaround for UI issues, you might want to reconsider creating the UI with a utility-first framework like Tailwind CSS instead. Tailwind provides you with an extensive collection of utility classes that enable you to efficiently build your own UI components.
Below is a quick demonstration of Tailwind CSS utility classes to implement a CTA link button, which you can see here in action:
<a href="#" class="text-white bg-indigo-500 border-0 py-3 px-8 focus:outline-none hover:bg-indigo-600 rounded-full text-lg mt-10 sm:mt-0"> A Link Button </a>
In contrast, Tailwind CSS requires some initial effort but makes it easier to create custom components without extensive CSS knowledge. A crucial aspect of this process involves gaining proficiency in effectively utilizing Tailwind CSS features and adhering to best practices for writing React components.
The key to building effective React components is to avoid building them prematurely. Creating abstractions too early can be challenging to undo. It’s advisable to begin with straightforward markup at the page level unless you have a clear understanding of what the ideal components will be.
Tailwind CSS facilitates this approach by enabling you to work quickly in a single file and develop the structure and style simultaneously.
The process of switching between multiple files can introduce unnecessary complications initially. As you gradually separate components, you can keep them within the same file until there is an actual need to use them elsewhere. Extraction should only occur when it becomes challenging to maintain the component in its current location.
A confusing component API can potentially result in bugs and code smells within your application. Robin Malfait, who now works at Tailwind Labs, once shared his “unwritten rules for components” in a Discord chat. Here are the main points of the discussion, which can assist you in selecting a well-designed component API.
Consider defining UI states clearly instead of using Booleans to switch states. Boolean-based UI states can result in numerous invalid permutations. In the explicit version, it is not possible to be both primary and secondary, or active and disabled at the same time, which minimizes state clashes and buggy UI:
<!-- Boolean-based UI States --> <Button primary disabled secondary active /> <!-- Well-defined UI States --> <Button state={Button.state.ACTIVE} variant={Button.variant.PRIMARY} />
When providing CSS classes as props for your components, it can make them implicit and, as a result, more prone to bugs. Determining the current UI state of such a component may become challenging if conflicting CSS classes are added to it later. This practice introduces code smell, and it is advisable to avoid it:
<!-- Avoid using CSS classes as props --> <Button state={Button.state.ACTIVE} className="bg-red-700" />
If state synchronization is not required, it is advisable to manage the state locally within a component instead of lifting it to the parent component. This helps avoid unnecessary complexity in the parent component.
Additionally, this approach ensures clarity about the source of the state, making it explicit and facilitating a better understanding of its origin:
function Counter() { const [count, setCount] = useState(0); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); }
Instead of storing the data as a component state, it is preferable to derive it directly from the props whenever possible. By utilizing props, the component gains increased flexibility and can be readily reused with different names:
function Greeting({ name }) { return <p>Hello, {name}!</p>; }
In some cases, it is preferable to have components that do not rely on any props. These components have a fixed behavior and do not have different states or variations based on props:
function LoadingSpinner() { return <div className="...">Loading...</div>; }
Instead of using props like <Button type="link" />
, it is recommended to simplify the implementation and have separate components that represent different usage variations. In this example, a <LinkButton />
component is created, which automatically renders a hyperlink as a CTA:
<!-- Complicated single component for two tasks --> const Button = ({ type }) => { return ( {type === 'button' ? ( <button className="...">...</button> ) : ( <a href="..." className="...">...</a> )} ); } <!-- Simplified separate components --> const Button = () => <button className="...">...</button> const LinkButton = () => <a href="..." className="...">...</a>
Instead of injecting data into the child elements through props, simplify the components by handling the data separately in the child elements themselves:
<!-- Component props to inject children data --> const Button = ({ label, icon }) => { return ( <button className="..."> {icon && <span className="...">{icon}</span>} {label} </button> ); } <!-- Simplified component to accept children openly --> const Button = ({ children }) => { return <button className="...">{children}</button>; }
One common question developers may come across is whether to use functional components for a better component API or rely on traditional class components.
Functional components are generally preferred over class components due to their simplicity, better performance, compatibility with React Hooks, and alignment with functional programming principles. It is advisable to prioritize using functional components as they keep the code simple and maintainable.
However, class components may still be relevant in legacy codebases or projects that require fine-grained control over component updates, lifecycle, or inheritance. Ultimately, the choice depends on the project’s requirements and considerations.
Let’s create a badge component in React and Tailwind CSS that offers four color variations and two sizes. I’m using TypeScript to demonstrate the efficiency of specifying the states. If you are using plain JavaScript, you can skip the types and enum parts.
First, let’s define some UI states before proceeding with the component creation and Tailwind CSS part. I’m using TypeScript enum classes to accomplish this:
enum BadgeVariant { ERROR, NOTE, SUCCESS, INFO, } enum BadgeSize { LARGE, SMALL, }
Now, let’s define some lookup tables or maps that contain the relevant CSS classes based on the styles and sizes specified in the enum classes mentioned above. These CSS classes are sourced from Tailwind CSS and provide utilities as defined in the Tailwind CSS documentation:
const SIZE_MAPS: Record<BadgeSize, string> = { [BadgeSize.SMALL]: 'px-2.5 text-xs', [BadgeSize.LARGE]: 'px-3 text-sm', }; const VARIANT_MAPS: Record<BadgeVariant, string> = { [BadgeVariant.ERROR]: 'bg-red-100 text-red-800', [BadgeVariant.NOTE]: 'bg-yellow-100 text-yellow-800', [BadgeVariant.SUCCESS]: 'bg-green-100 text-green-800', [BadgeVariant.INFO]: 'bg-blue-100 text-blue-800', };
Now, let’s move on to the component creation part. First, we’ll define a type for the props of our component. Then, we’ll proceed to create our badge component, utilizing the prop values and the state maps defined earlier.
For smaller projects, React PropTypes can be a valuable choice due to their ability to provide runtime type checking. However, for larger projects where robust type checking and an improved developer experience are desired, TypeScript’s static types are often a better fit:
type BadgeProps = { variant: BadgeVariant; children?: ReactNode; size: BadgeSize; }; export default function Badge(props: BadgeProps) { const { children, variant, size } = props; const badgeLayoutClasses = 'inline-flex items-center px-2 py-1 rounded-full font-medium leading-none'; const finalBadgeClasses = `${badgeLayoutClasses} ${VARIANT_MAPS[variant]} ${SIZE_MAPS[size]}`; return <span className={finalBadgeClasses}>{children}</span>; }
Finally, let’s define default values for our badge component. The most convenient way to achieve this in React is by utilizing the built-in defaultProps
property. By setting default values for specific props, defaultProps
ensures that when a component is used without explicitly passing those props, the default values will be used instead:
Badge.defaultProps = { variant: BadgeVariant.INFO, size: BadgeSize.SMALL, };
Additionally, we can define prop extensions that seamlessly integrate with the component. This approach ensures that prop options remain intuitive and promotes safe usage of the component:
Badge.variant = BadgeVariant; Badge.size = BadgeSize;
All the sections we discussed above are consolidated into a single file, implementing the principles mentioned in the previous segment. See the above code in action here:
vitejs-vite-22e5cy – StackBlitz
Next generation frontend tooling. It's fast!
To provide some insight on the previously mentioned implementation, it is important to highlight a few points:
className
prop is not used, ensuring that the component’s states and variants are well-defined without introducing implicit statesWhen encapsulating patterns or snippets that may not need to stay linked to other instances, they are considered templates or snippets instead of components. An example of this is Tailwind UI, which provides snippets for accelerating prototyping. If these snippets are repeated consistently throughout your app, they can be transformed into reusable components with a clear API. Otherwise, they can remain in your snippet library.
A button is a typical example of a small component that often depends more on CSS styles than JavaScript logic. In the example provided, there are 17 utility classes. It’s possible to have multiple buttons across a website that share 15 out of the 17 classes:
<button type="button" class="inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"> Button text </button>
To address this, one approach is to utilize the Tailwind @apply
directive and combine the common classes into a new btn
class. However, it’s worth noting that some developers prefer to keep most of these components within the React component model as it provides a clearer mental model and easier customization.
Complex components like app shells, sidebars, or headers can become overly parameterized if you try to make them highly configurable. Instead, it is recommended to prioritize components over props and children over components. Initially, these large components may be similar, but as the site grows, each instance tends to diverge, leading to the addition of more props. If a large component becomes unwieldy, it is better to break it down into smaller sub-components for better maintainability.
Using React is an excellent approach for developing contemporary applications based on components. Tailwind CSS provides a straightforward solution for styling applications without requiring an extensive understanding of CSS. When combined, these two technologies empower you to construct reusable components that harmoniously blend aesthetics and functionality.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.