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: {children}
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.
but shadcn has bacicaly mix of predefined variants and you can add styles using className on top so bacically both aproaches that showed in this video, aint it?
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! :)
Thanks for the comment + kind words! I agree that wasn't the most clear - I could have used since that's more common, but regardless of that, is still a common and (often) a great abstraction for an app. The reason I chose it is because it's often the first thing folks reach for, and when all you have is a hammer... is great if what you're trying to do is share styles, but if you're not, it can get you into trouble and make new pages unnecessarily difficult to build. I do like using just `className` for buttons as well as all other UI elements when I'm starting a project, because trying to force all new screens into an API you need to guess, before you even see the actual UI and how it's going to change, is a recipe for a ton of bad abstractions. I think I'm going to do a follow-up video to talk more about all of this :)
Imho, this is the way to go, then if you want all variations of the button styled, you could just create a component that does the same thing as you saw here, except has all the styles hidden behind a prop such as variant. Or maybe separate components for each style of button. This would probs be the best way, since later when the spec changes and more often than not it does, you can still use your loading logic and create another button or whatever you like. Separate the logic from the styles. Have components that represent behavior, and others that can represent specific styling. This is best in my experience. Great video as always!
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 React.ButtonHTMLAttributes { loading?: boolean; loader?: React.ReactNode; children: React.ReactNode; } const Button: React.FC = ({ loading = false, loader, children, className, ...props }) => { return (
{loading ? {loader || 'Loading...'} : children}
); }; export default Button; *Usage Example* inside a form use it like this:
Submit Regular use: Regular 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"
You can still style your unstyled components. The point if to create the component to be extendable. Just like a headlesss ui library does it. It's even better because it makes it easier to later tweak your design system without worrying about functionality.
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!
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.
this doesn’t at all get in the way of this. literally just add the code he removed back lol. dude basically showed you how trivial it is implement the like 20ish SLoC that makes shadcn components more then radix primitives.
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.
Yes - there are definitely other patterns that enable even more composition! I don't always think the most composable solution is the best however - composition comes with a cost. But for this particular example I really like the API that the Radix team came up with here: www.radix-ui.com/themes/docs/components/spinner I went with for this video to keep things simple :) Plenty of space in the course to discuss these further nuances though!
@@samselikoff Totally agree on the cost, I just mentioned it as an alternative (for others). I like having separate components that do just styling and components for layout, that way I can then mix and match them with minimal amount of overlap. Modules then have some very specific components, such as a SubmitButton, which is then hooked to the form + PrimaryButton, containing Horizontal, containing P and Icon.
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.
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
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: ; 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: ; ; ; 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.
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 )✌
Again, nice video! Even though I feel like color is not the best example to choose here because you don’t want to allow 20 individually colored buttons but only a specified set of like 4 different colors imo, so putting it into the button component like you did. But completely agree with the approach in general 👍🏻
Great question! Compound components is my favorite pattern for this - I'll be covering it in the course but check out Radix for an example of what it looks like: www.radix-ui.com/primitives/docs/components/dropdown-menu#anatomy
It's a good idea, I was struggling with this yesterday evening (copy pasting my loading spinner everywhere) But the button you made may be improved, using functions like twMerge or cn from shadcn By passing relatives as first argument to these functions we will be making sure that if user uses absolutely or fixed positioned Buttons, our logic is still working...
So now you will have to define all the styles outside of component ? What if you use this button green 1000 times. 1000 x green classes (+all other classes that comes with this variant) ? Instead of just passing a param "green" which corresponds to component that was defined by design system? This doesn't look right.
Yes, it's a good point - if I had 3 of the same green button, and the duplication was painful, it would definitely be time to extract a component! The point of this vid was to show a technique for when you *don't* have 1000 buttons, but instead have 2 completely different ones, and want to share some internal behavior that doesn't have to do with styling. I think I'll make a follow-up video because I agree that with a color/variant prop example is not the most clear. Definitely in favor of components when the situation calls for it 👍
@@samselikoff This video reminds me of ShadCN components. They have default styles hardcoded into the component and controllable with themes, with the ability to override classname when calling the component.
Thanks for the clear breakdown. Was cool to see your thought process. For passing styles without Tailwind classes, I think we could just pass props of CSSProperties type, is that correct? So in the implementation of the LoadingButton it would be like: and then in the component, we could replace "className: string;" with "styles: CSSProperties;" and then implement in the returned button with ?
I don’t think a button is a good use case for what you’re explaining. A button is used everywhere with a handful but limited set of variations. Now I have to style it every time I need it.
I dont get it why you talking about unstyled components and styling it. Usually unstyled components are used in component libs and they don't have this issue. If you want to use a styled lib, it is absolut fine to pass a color prop because you want to lock the styling as the lib owner for an consistent look and feel.
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:
{children}
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.
but shadcn has bacicaly mix of predefined variants and you can add styles using className on top
so bacically both aproaches that showed in this video, aint it?
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! :)
Thanks for the comment + kind words!
I agree that wasn't the most clear - I could have used since that's more common, but regardless of that, is still a common and (often) a great abstraction for an app. The reason I chose it is because it's often the first thing folks reach for, and when all you have is a hammer...
is great if what you're trying to do is share styles, but if you're not, it can get you into trouble and make new pages unnecessarily difficult to build. I do like using just `className` for buttons as well as all other UI elements when I'm starting a project, because trying to force all new screens into an API you need to guess, before you even see the actual UI and how it's going to change, is a recipe for a ton of bad abstractions.
I think I'm going to do a follow-up video to talk more about all of this :)
Imho, this is the way to go, then if you want all variations of the button styled, you could just create a component that does the same thing as you saw here, except has all the styles hidden behind a prop such as variant. Or maybe separate components for each style of button.
This would probs be the best way, since later when the spec changes and more often than not it does, you can still use your loading logic and create another button or whatever you like.
Separate the logic from the styles. Have components that represent behavior, and others that can represent specific styling. This is best in my experience.
Great video as always!
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.
I'm so glad I found you channel
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 React.ButtonHTMLAttributes {
loading?: boolean;
loader?: React.ReactNode;
children: React.ReactNode;
}
const Button: React.FC = ({ loading = false, loader, children, className, ...props }) => {
return (
{loading ? {loader || 'Loading...'} : children}
);
};
export default Button;
*Usage Example*
inside a form use it like this:
Submit
Regular use:
Regular 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"
Good one, Also tailwind-merge and clsx is perfect for such use cases
Or if you don't want another dependency ```const join = (...parts: (string | undefined)[]) => parts.filter(Boolean).join(" ");```
love this setup been using for all my latest works
please do more react patterns videos like this ♥♥♥♥
For design systems Premature abstraction is the key for consistent UI
,
You can still style your unstyled components. The point if to create the component to be extendable. Just like a headlesss ui library does it. It's even better because it makes it easier to later tweak your design system without worrying about functionality.
@@kevinbatdorf it make it easier for devs across the team to mess around design system consistency
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!
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.
this doesn’t at all get in the way of this. literally just add the code he removed back lol. dude basically showed you how trivial it is implement the like 20ish SLoC that makes shadcn components more then radix primitives.
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.
Yes - there are definitely other patterns that enable even more composition! I don't always think the most composable solution is the best however - composition comes with a cost. But for this particular example I really like the API that the Radix team came up with here: www.radix-ui.com/themes/docs/components/spinner
I went with for this video to keep things simple :) Plenty of space in the course to discuss these further nuances though!
@@samselikoff Totally agree on the cost, I just mentioned it as an alternative (for others).
I like having separate components that do just styling and components for layout, that way I can then mix and match them with minimal amount of overlap. Modules then have some very specific components, such as a SubmitButton, which is then hooked to the form + PrimaryButton, containing Horizontal, containing P and Icon.
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.
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
Looking forward to a new creative video ❤
Greetings from Cyprus. Great content as always :)
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:
;
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:
;
;
;
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.
What is your mic setup it sounds amazing
Good stuff
we need a nvim tutorial as well🎉🎉
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?
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 )✌
Cool analyze, reasonable
I’d watch a video just on how you navigate the keyboard, various files in VSC, and shortcuts 😂 superb
My gf is really frustrated with my premature abstraction! Oh wait, no sorry that is something else
You're a joker huh😂😂😂
Again, nice video!
Even though I feel like color is not the best example to choose here because you don’t want to allow 20 individually colored buttons but only a specified set of like 4 different colors imo, so putting it into the button component like you did.
But completely agree with the approach in general 👍🏻
What about more complex components that have multiple layers of sub-components to potentially override, rather than just one level?
Great question! Compound components is my favorite pattern for this - I'll be covering it in the course but check out Radix for an example of what it looks like: www.radix-ui.com/primitives/docs/components/dropdown-menu#anatomy
this is a lot of thought just to avoid using CSS
hi sam, I think it will be interested if you can make a file uploader dropzone component with react and framer motion!
It's a good idea, I was struggling with this yesterday evening (copy pasting my loading spinner everywhere)
But the button you made may be improved, using functions like twMerge or cn from shadcn
By passing relatives as first argument to these functions we will be making sure that if user uses absolutely or fixed positioned Buttons, our logic is still working...
EVERYTIME I watch your coding videos my IQ goes up a bit ;) AWESOME video
So now you will have to define all the styles outside of component ? What if you use this button green 1000 times. 1000 x green classes (+all other classes that comes with this variant) ? Instead of just passing a param "green" which corresponds to component that was defined by design system?
This doesn't look right.
Yes, it's a good point - if I had 3 of the same green button, and the duplication was painful, it would definitely be time to extract a component! The point of this vid was to show a technique for when you *don't* have 1000 buttons, but instead have 2 completely different ones, and want to share some internal behavior that doesn't have to do with styling.
I think I'll make a follow-up video because I agree that with a color/variant prop example is not the most clear. Definitely in favor of components when the situation calls for it 👍
@@samselikoff This video reminds me of ShadCN components. They have default styles hardcoded into the component and controllable with themes, with the ability to override classname when calling the component.
Thanks for the clear breakdown. Was cool to see your thought process.
For passing styles without Tailwind classes, I think we could just pass props of CSSProperties type, is that correct?
So in the implementation of the LoadingButton it would be like:
and then in the component, we could replace "className: string;" with "styles: CSSProperties;" and then implement in the returned button with ?
I don’t think a button is a good use case for what you’re explaining. A button is used everywhere with a handful but limited set of variations. Now I have to style it every time I need it.
hey you are looking like jos buttler england cricket player
Pretty nice video! Also you can ommit explicitly naming the className and children from the LoadingButton props as ComponentProps come with those :D
watching from free 🇧🇩
I dont get it why you talking about unstyled components and styling it. Usually unstyled components are used in component libs and they don't have this issue. If you want to use a styled lib, it is absolut fine to pass a color prop because you want to lock the styling as the lib owner for an consistent look and feel.
I think you meant to write "premature abstractulation" HEHEHEHH EHHEHE HHEHHE
It's true that design might be questionable but it has nothing to do with "abstraction", u've used wrong word here.
How come?
I think it has everything to do with abstraction. It’s the correct word.
An abstraction is a box. It can contain data like styles.
The concept of the Button and its prop design was the abstraction.