Avoid premature abstraction with Unstyled Components

Поділитися
Вставка

КОМЕНТАРІ • 58

  • @lilfilda9424
    @lilfilda9424 3 місяці тому +14

    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}

  • @Shishir.435
    @Shishir.435 3 місяці тому +3

    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.

    • @27sosite73
      @27sosite73 Місяць тому

      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?

  • @kushagragour
    @kushagragour 3 місяці тому +18

    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! :)

    • @samselikoff
      @samselikoff  3 місяці тому +10

      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 :)

    • @thejugovic
      @thejugovic 3 місяці тому

      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!

  • @TannerBarcelos
    @TannerBarcelos 3 місяці тому +2

    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.

  • @thelaitas
    @thelaitas Місяць тому

    I'm so glad I found you channel

  • @FLICITY
    @FLICITY 3 місяці тому

    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"

  • @rjtdas
    @rjtdas 3 місяці тому +25

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

    • @miran248
      @miran248 3 місяці тому

      Or if you don't want another dependency ```const join = (...parts: (string | undefined)[]) => parts.filter(Boolean).join(" ");```

    • @abhishekparmar4983
      @abhishekparmar4983 3 місяці тому +1

      love this setup been using for all my latest works

  • @regilearn2138
    @regilearn2138 3 місяці тому +1

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

  • @benrandjaakram6760
    @benrandjaakram6760 3 місяці тому +14

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

    • @kevinbatdorf
      @kevinbatdorf 3 місяці тому

      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.

    • @benrandjaakram6760
      @benrandjaakram6760 3 місяці тому +1

      @@kevinbatdorf it make it easier for devs across the team to mess around design system consistency

  • @DRCmusic
    @DRCmusic 3 місяці тому

    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!

  • @aliventurous
    @aliventurous 3 місяці тому +1

    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.

    • @trejohnson7677
      @trejohnson7677 2 місяці тому

      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.

  • @miran248
    @miran248 3 місяці тому +1

    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.

    • @samselikoff
      @samselikoff  3 місяці тому +1

      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!

    • @miran248
      @miran248 3 місяці тому

      @@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.

  • @trejohnson7677
    @trejohnson7677 2 місяці тому

    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.

  • @acloudonthebluestsky9687
    @acloudonthebluestsky9687 3 місяці тому

    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

  • @harisamjad-pro
    @harisamjad-pro 2 місяці тому

    Looking forward to a new creative video ❤

  • @alexpanteli3651
    @alexpanteli3651 3 місяці тому

    Greetings from Cyprus. Great content as always :)

  • @joshuagalit6936
    @joshuagalit6936 3 місяці тому

    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.

  • @lih4553
    @lih4553 3 місяці тому +1

    What is your mic setup it sounds amazing

  • @chrissalgaj4111
    @chrissalgaj4111 3 місяці тому

    Good stuff

  • @kakun7238
    @kakun7238 3 місяці тому

    we need a nvim tutorial as well🎉🎉

  • @alarsut
    @alarsut 3 місяці тому

    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?

  • @NOOBISTGAMER
    @NOOBISTGAMER 3 місяці тому

    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 )✌

  • @havefun5519
    @havefun5519 3 місяці тому

    Cool analyze, reasonable

  • @DevinCLane
    @DevinCLane 3 місяці тому

    I’d watch a video just on how you navigate the keyboard, various files in VSC, and shortcuts 😂 superb

  • @jamesgulland
    @jamesgulland 3 місяці тому +20

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

  • @chrizzdf7099
    @chrizzdf7099 3 місяці тому +1

    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 👍🏻

  • @BritainRitten
    @BritainRitten 3 місяці тому

    What about more complex components that have multiple layers of sub-components to potentially override, rather than just one level?

    • @samselikoff
      @samselikoff  3 місяці тому +1

      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

  • @TeHzoAr
    @TeHzoAr 3 місяці тому +1

    this is a lot of thought just to avoid using CSS

  • @naufaldoesvlogg
    @naufaldoesvlogg 3 місяці тому

    hi sam, I think it will be interested if you can make a file uploader dropzone component with react and framer motion!

  • @gopallohar5534
    @gopallohar5534 3 місяці тому +1

    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...

  • @osman3404
    @osman3404 3 місяці тому

    EVERYTIME I watch your coding videos my IQ goes up a bit ;) AWESOME video

  • @coolemur976
    @coolemur976 3 місяці тому

    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.

    • @samselikoff
      @samselikoff  3 місяці тому +1

      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 👍

    • @desitdt
      @desitdt 3 місяці тому

      @@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.

  • @devoptimist
    @devoptimist 3 місяці тому

    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 ?

  • @cb73
    @cb73 3 місяці тому

    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.

  • @Asyedabdulrahman33
    @Asyedabdulrahman33 3 місяці тому

    hey you are looking like jos buttler england cricket player

  • @IDOLIKIofficial
    @IDOLIKIofficial 3 місяці тому +1

    Pretty nice video! Also you can ommit explicitly naming the className and children from the LoadingButton props as ComponentProps come with those :D

  • @raiyansarker
    @raiyansarker 3 місяці тому +2

    watching from free 🇧🇩

  • @ManuelFischer-u5m
    @ManuelFischer-u5m 2 місяці тому

    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.

  • @keepforever726
    @keepforever726 3 місяці тому

    I think you meant to write "premature abstractulation" HEHEHEHH EHHEHE HHEHHE

  • @RaZziaN1
    @RaZziaN1 3 місяці тому +5

    It's true that design might be questionable but it has nothing to do with "abstraction", u've used wrong word here.

    • @KritX01
      @KritX01 3 місяці тому +2

      How come?

    • @chairlovawitabat
      @chairlovawitabat 3 місяці тому +5

      I think it has everything to do with abstraction. It’s the correct word.

    • @0xSLN
      @0xSLN 3 місяці тому

      An abstraction is a box. It can contain data like styles.

    • @havokgames8297
      @havokgames8297 3 місяці тому

      The concept of the Button and its prop design was the abstraction.