Avoid premature abstraction with Unstyled Components

preview_player
Показать описание
As programmers we love abstractions – but if you abstract the wrong part of your code, the cure is worse than the disease. Learn how unstyled React components can help you extract smaller chunks of logic without jumping straight to a premature, overly abstracted component.

Timestamps:

0:00 - Intro
2:10 - Extracting a Button
5:41 - The problem with our abstraction
8:24 - Extracting an Unstyled Component
12:05 - Using ComponentProps and the JSX spread syntax
14:30 - Final demo
16:39 - Advanced React Component Patterns
Рекомендации по теме
Комментарии
Автор

The video presents a valuable approach, but an alternative implementation could enhance flexibility and usability. I would design a button component capable of accepting parameters for color, size, and state (such as 'loading'). If no parameters are provided, the button would default to a static state. Additionally, I would incorporate a className prop to allow for custom styling and enable nesting of content within the button. An example of this refined component might be structured as follows: <Button color='blue' variant='loading' size='sm' className='font-mono'>
{children}
</Button>

lilfilda
Автор

This is the reason why I love shadcn implementation of components.
I learned a lot from their implementation. As in this video, we can use tw-merge and clsx to be really sure that the tailwind classes are applied as we wanted them to be.

Shishir.
Автор

I get the idea of unstyled components, but I think you chose a wrong example to demonstrate it. When making a button, one would never want to pass in colors (that too a bunch of tailwind) classes from outside. That is like giving an ability to put any random classes on it. Now the LoadingButton could be used inside a Button component, bu then it doesn't need to publicly exported. Also, you mentioned an issue where you need to keep adding new color options to the button. That is definitely a problem which should be solved in the design of the website, not by providing a "do anything" className prop on some component.
Always love your videos! :)

kushagragour
Автор

Glad this showed up. I realize I did a lot of premature abstractions in my code base at work. I think I’m going to do some refactors tomorrow.

TannerBarcelos
Автор

Good one, Also tailwind-merge and clsx is perfect for such use cases

rjtdas
Автор

Your videos are great. Your D3 line chart video convinced me to pick up tailwind on all my projects and delve into d3.js. Thanks for sharing all your knowledge!

DRCmusic
Автор

Hey, I love your point about the pre-mature abstraction. Great work.
For the button component here's how I would implement it:

*use a utility function* (*cred to shad-cn*)

import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

*Button Component (Button.tsx)*

import React from 'react';
import { cn } from './cn'; // Your path goes here

interface ButtonProps extends {
loading?: boolean;
loader?: React.ReactNode;
children: React.ReactNode;
}

const Button: React.FC<ButtonProps> = ({ loading = false, loader, children, className, ...props }) => {
return (
<button
type="button"
className={cn(
'w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white',
loading ? 'bg-indigo-300' : 'bg-indigo-600 hover:bg-indigo-700',
'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500',
className
)}
disabled={loading}
{...props}
>
{loading ? <span>{loader || 'Loading...'}</span> : children}
</button>
);
};

export default Button;


*Usage Example*

inside a form use it like this:

<Button className="bg-green-600 hover:bg-green-900" type="submit" loading={loading} loader={<Loader />}>
Submit
</Button>

Regular use:
<Button>Regular Button</Button>




P.S. I know the button can be optimized in so many ways, but the important point is "how the styling is handled with the utility function"

FLICITY
Автор

For design systems Premature abstraction is the key for consistent UI
,

benrandjaakram
Автор

please do more react patterns videos like this ♥♥♥♥

regilearn
Автор

New Here, I saw the Recursive Video and just checked if I should Subscribe, but your video and explanation are nice and easy to understand for even stupids, so I just Subscribed...!😇 Keep making coding related video ( React JS, Next JS or whatever, I will watch it if you explan like this )✌

NOOBISTGAMER
Автор

Greetings from Cyprus. Great content as always :)

alexpanteli
Автор

Now imagine you would like the button to have some default styles so you don't have to style the button every time you call it. And to have the ability to override those styles at the calling site. And to be able to use the button as a link. And variants. This is why I love using shadcn ui. It has a design system built in. The shadcn button is one of the first things I install when starting a new project.

aliventurous
Автор

Imagine you're building an Icon component. As part of the component's API, you want users to be able to specify the color of the icon.

Your brand has some known colors, like primary and secondary. But you also want to make sure that users can specify any color they want.

You might start by defining a Color type:

type Color = "primary" | "secondary" | string;
Then, using that type in your Icon component:

type IconProps = {
color: Color;
};

const Icon = ({ color }: IconProps) => {
// ...
};
Then, you might use the Icon component like this:

<Icon color="primary" />;
But there's an issue. We aren't getting color suggestions when we use the Icon component. If we try to autocomplete the color prop, we get no suggestions.

Ideally, we want primary and secondary to be part of that list. How do we manage that?

The Solution
The solution is very odd-looking. We can intersect the string type in Color with an empty object:

type Color = "primary" | "secondary" | (string & {});
Now, we'll get suggestions for primary and secondary when we use the Icon component.

What on earth?

Why It Works
This works because of a quirk of the TypeScript compiler.

When you create a union between a string literal type and string, TypeScript will eagerly widen the type to string. We can see this by hovering over Color without the intersection:

type Color = "primary" | "secondary" | string;
// 🚁

// 🚁 Hovering over `Color` shows...
type Color = string
So, before it's ever used, TypeScript has already forgotten that "primary" and "secondary" were ever part of the type.

But by intersecting string with an empty object, we trick TypeScript into retaining the string literal types for a bit longer.

type Color = "primary" | "secondary" | (string & {});
// 🚁

// 🚁 Hovering over `Color` shows...
type Color = "primary" | "secondary" | (string & {})
Now, when we use Color, TypeScript will remember that "primary" and "secondary" are part of the type - and it'll give us suggestions accordingly.

string & {} is actually exactly the same type as string - so there's no difference in what types can be passed to our Icon component:

<Icon color="primary" />;
<Icon color="secondary" />;
<Icon color="#fff" />;
This Looks Pretty Fragile...
You might think that this is a pretty fragile solution. This doesn't seem like intended behavior from TypeScript. Surely, at some point, this will break?

Well, the TypeScript team actually know about this trick. They test against it.

Someday, they may make it so that a plain string type will remember string literal types. But until then, this is a neat trick to remember.

joshuagalit
Автор

After a few projects, i realized how pre-styling can be pretty painful.
It's consistent but in the other hands, limit our options to do with the component

acloudonthebluestsky
Автор

its wasn’t really the styling that got in the way, but in this case it was useful to abstract over the behavior first. i think order of abstraction isn’t a detail that’s typically designed for explicitly, and that decreases DX by a significant factor. in short, what it’s doing should be resolved before resolving what it looks like doing it.

trejohnson
Автор

You could also make it a function (renderSpanWithSpinner for example), which would return a fragment instead of a button, that way you could use anchors and other elements, and they could then be separate components with no loading functionality (each component should do just one thing).
Composition is the key, imagine if you had a a button with a spinner, icon and a text - all this, plus layout should be explicit, no props; then if you need to reuse it you can just make another specialized component (for consistency).
Atomic design with headless components (or with minimal styling - unstyled) is my fave way to do things, it does require discipline though :)
It's much easier to make a single Button component which takes a shitton of different, sometimes conflicting props which do layout and / or styling and call it a day.

miran
Автор

Looking forward to a new creative video ❤

harisamjad-pro
Автор

My gf is really frustrated with my premature abstraction! Oh wait, no sorry that is something else

jamesgulland
Автор

hi sam, thanks for your great video!

could you make a video about best practices for nextjs layout with rolebased ui? or where to check auth?

alarsut
Автор

What is your mic setup it sounds amazing

lih