DEV Community

Cover image for Why I'm not a fan of Single File Components
Ryan Carniato
Ryan Carniato

Posted on • Edited on

Why I'm not a fan of Single File Components

Single File Components(SFCs) are a style of application organization used by JavaScript UI libraries where each file represents a single component in all aspects. Typically they resemble an HTML document where you have HTML tags, Style Tag, and Script Tag all in a file. This is the common pattern for UI Frameworks like Vue and Svelte.

I was looking for some good literature on the subject and I found a lot of people talking about the separation of concerns. I am not advocating strict adherence of MVC and keeping your code and view separate from my styles etc... Nor am I advocating having component files exporting more than one component.

I want to talk about the limitation of SFCs as a component format. To me, this topic is a lot like discussing the benefits of Hooks over Class lifecycles. I believe there are clear unequivocal benefits of not using typical SFCs.

Component Boundaries

What is a component? What is the logical breakdown of what should be a component? This is not obvious to anyone at the beginning, and it continues to be difficult even as you gain more experience.

One might argue that school taught them that the Single Responsible Principle means a Component should do exactly one thing. And maybe that's a reasonable heuristic.

I know a beginner might not even want to fuss with that. Stick way too much in one component so all their code is in front of them. They aren't messing with "props", "events", "context" or any other cross-component plumbing. Just simple code.

Some frameworks might even have very strict reasons for component boundaries if they are tied to the change propagation system (like all VDOM libraries). They define what re-renders or not. shouldComponentUpdate is not something that exists without having severe repercussions for messing with component boundaries.

So what should make a component?

Ideally, whatever makes sense for the developer. Rich Harris, creator of Svelte, in talking about disappearing frameworks once said, "Frameworks are there to organize your mind". Components are just an extension of that.

So SFCs actually handle this pretty well. No problem so far. But let's dig deeper.

Component Cost

I did some pretty deep performance testing of the Cost of Components in UI libraries. The TL;DR is for the most part VDOM libraries like React scale well with more components whereas other libraries especially reactive libraries do not. They often need to synchronize reactive expressions with child component internals which comes at a small cost.

Go look at a benchmark with reactive libraries and VDOM libraries and look at how they use components differently. How often do the Reactive libraries use more than a single component when testing creation cost? In real apps, we happen to have lots.

Aside: I wrote a benchmark implementation with a React-Like library using some child components that I wrote with less code than Svelte. I mentioned it in an article and was met with that it wasn't fair that I wasn't making separate components for Svelte. But I couldn't because I wanted to show off Svelte's performance.

Where am I going with this? It isn't simple enough to congratulate the type of libraries that use SFCs for not forcing esoteric components on us.

Component Refactoring

What is the most expensive part of refactoring? I'd personally nominate redefining boundaries. If our ideal components are those that let us choose the boundaries we want, I'd propose our components should grow and split apart at our comfort.

React's Component model is actually pretty convenient for this. Starting with being able to have more than one component in a single file. When something gets a bit unwieldy we just break it off.

It might be as simple to make the template more readable. Maybe just to reduce repetition. Kind of like that natural point where you decide to break something into its own function. I mean how do you write less code in JavaScript? You write a function.

Let's put this another way. Picture how you would do this in the library of your choice(I'm going to use React). Pretend you have a Component that produces a Side Effect like maybe uses a Chart Library and cleans up after.

export default function Chart(props) {
  const el = useRef();
  useEffect(() => {
    const c = new Chart(el.current, props.data);
    return () => c.release();
  }, []);
  return (
    <>
      <h1>{props.header}</h1>
      <div ref={el} />
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

Now you have a new requirement to make it conditionally apply based on a boolean enabled prop.

If you went through this exercise and you kept it as a single component, you should realize that to apply the conditional you end up applying it in both the view and the imperative portions of the code (mount, update, and release).

export default function Chart(props) {
  const el = useRef();
  useEffect(() => {
    let c;
    if (props.enabled) c = new Chart(el.current, props.data);
    return () => if (c) c.release();
  }, [props.enabled]);

  return (
    <>
      <h1>{props.header}</h1>
      {props.enabled && <div ref={el} />}
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

Or using React you simply broke it into another Component and the logic stays more or less the same.

function Chart(props) {
  const el = useRef();
  useEffect(() => {
    const c = new Chart(el.current, props.data);
    return () => c.release();
  }, []);
  return <div ref={el} />;
}

export default function ChartContainer(props) {
  return (
    <>
      <h1>{props.header}</h1>
      {props.enabled && <Chart data={props.data} />}
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

This is a simple example but this sort of one change touch multiple points is the same reason Hooks/Composition API/Svelte $ can produce more compact and easier maintainable code than class lifecycles. Yet here we are asking the same difference of our template vs our JavaScript.

This is true of not only side effects, but nested state as well. The nicest part of the React approach here is it is non-commital. I didn't need to make a new file. I'm still learning how this component works. What if the requirements change again? What if I'm that newbie just learning the ropes?

The limitation of SFCs

The crux of the problem with restricting files to a single component is we only get a single level of state/lifecycle to work with. It can't grow or easily change. It leads to extra code when the boundaries are mismatched and cognitive overhead when breaking apart multiple files unnecessarily.

SFCs libraries could look at ways to do nested syntax. Most libraries. even non-SFC ones, don't support this though. React for instance doesn't allow nesting of Hooks or putting them under conditionals. And most SFCs don't really allow arbitrary nested JavaScript in their templates. MarkoJS might be the only SFC one that I'm aware of supporting Macros(nested components) and inline JS, but that is far from the norm.

Maybe you don't feel it's important enough but there is value for the beginner to the expert in an application architecture built with maintainability in mind from day one. It progressively grows with them. And that's why I dislike SFCs the same way I prefer Hooks over Class Components.


And it's why SolidJS is designed to have the best experience as you grow your applications. It's components live up to the ideal. It is the best of both worlds. It does not force you to make a bunch of unnecessary components like a VDOM library, but doesn't restrict you from doing so. Supports nested state and effects in the templates so it grows with you.

In another words in addition to the ways mentioned above you can nest effects and state. You can even use a ref callback to do this sort of inline custom directive:

export default function Chart(props) {
  return (
    <>
      <h1>{props.header}</h1>
      {
        props.enabled && <div ref={el =>
          createEffect(() => {
            const c new Chart(el.current, props.data);
            onCleanup(() => c.release());
          })
        } />
      }
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

Solid achieves this with Declarative Data independent of the lifecycle, disappearing components, JSX powered templates and high-performance granular reactivity.

Hooks and Composition API only just scratch the surface of what you can do with declarative data patterns. Come check out the most familiar yet starkly different JS(TypeScript) Framework.

https://github.com/ryansolid/solid

Top comments (10)

Collapse
 
stevetaylor profile image
Steve Taylor

Having used Svelte for quite a while now, I generally disagree with all of this. My experience with component refactoring is that it's not painful at all. The beauty of SFCs is they make a whole bunch of boilerplate go away. For example, here's a SolidJS component that renders nothing:

HelloWorld.js

export default function HelloWorld() {
  return null;
}
Enter fullscreen mode Exit fullscreen mode

and here's the equivalent in Svelte:
HelloWorld.svelte

Enter fullscreen mode Exit fullscreen mode

I think the ideal sweet spot for Solid would be to support both SFCs (e.g. HelloWorld.solid) and non-SFC components.

With the upcoming Svelte 5 providing a new syntax (runes) for reactivity that seems to make it more similar to Solid in terms of syntax and performance, I think the time is right to reconsider this position.

Collapse
 
ryansolid profile image
Ryan Carniato • Edited

Not really. There are 3rd party SFC libraries for Solid if people want to do that, but as it isn't a requirement I don't really care that much. I say this having worked on Marko Tag API which is the most refactorable SFC format I can think of. Has the least amount of code out of any framework. Basically Svelte's less code/boilerplate to the extreme. But you are crossing a line. There is a specific tradeoff here. In Marko's case everything composable piece is a .marko file. It's consistent.

My experience is the boilerplate argument sells best in small demos/examples. Whereas the control and flexibility pays off dividends on scaling. Unfortunately we only are going to talk in small examples so that's fine, but like a trivial component being 3 lines versus one identifier doesn't really represent anything. Like Vue does (and Marko) do autoimport statements. That has the same level of code reduction in hello world, but I do wonder about what we lose there.

So sure Svelte doesn't have the performance implications with Svelte 5 because they went to fine grained rendering like Solid. But the syntax for reactivity to get into the template requires something special, and now they are running JS level transformation over all your source files. I think that's probably fine but to accomplish the pattern they wanted they had to bring the SFC magic everywhere, at which point if it is everywhere anyway, why do we need a special file for it?

Let's continue to watch how Svelte develops as patterns encourage more functional composition because of Runes. I don't view this as progression as a sign that SFCs need reconsideration from our point, but rather from Svelte's perspective as they continue to adopt patterns more similar to Solid.

Collapse
 
richarddavenport profile image
Richard Davenport • Edited

I love these types of discussions. By day I am an Angular developer, but at night I use Svelte. I was a react developer for some time, and I could never get over JSX, or the irony of react not being reactive. I would rather write templates. Solid looks like it would be great to try, I'm just not that into JSX. Anyway else to try it?

Collapse
 
ryansolid profile image
Ryan Carniato

Technically I support Tagged Template Literals, but it is bit of a pale imitation. None of the compiler goodness since I find it inconsistent to have Tagged Template Literals with the same syntax not work runtime only. So that means manually wrapping expressions and opting out of some performance optimizations. To be fair it is still the fastest Tagged Template implementation out there as it JIT compiles to similar code to Solid's JSX, and still outperforms all the popular libraries in benchmarks. But it means worse DX and bigger bundle size as can't aggressively Tree Shake. Solid uses JSX the way Svelte uses templates in it completely changes what is there and optimizes bundles based on usage. The output is nothing like a HyperScript or a VDOM library.

I understand the dislike for JSX, but all the toolings already there (Parsers, Syntax Highlighting, Babel, Prettier, TypeScript) from day one. I don't think Solid would be so fully featured today if it weren't for me being able to avoid completely re-inventing the toolchain. It's taken Vue/Svelte years and it still is WIP. Honestly I'm a bit surprised more non-VDOM libraries didn't go this way. To build a Template engine like those with that level of sophistication would take me years by myself.

Collapse
 
richarddavenport profile image
Richard Davenport

I completely agree with you. I think Solid looks awesome, and if I decide to give JSX another shot Solid is what I'll be using. Thanks for your work!

Collapse
 
steakeye profile image
Andrew Keats

Considering using SolidJS on my next personal project 👍

Collapse
 
bingalls profile image
Bruce Ingalls

@ryansolid It looks that SolidJS was intended to work with React. Will it work well with other lib/frameworks?
For other libs, it seems that SFCs are effective for leaf components, that don't need to send (state) change events to parents.
An additional issue with SFCs that bundle CSS, is that CSS is global in nature. Vue accommodates locally scoped CSS, but it is not as nice as plain/global CSS work with. I'd not thought through, this impact in React. If not clear, one component may have a different definition of blue compared to another, for example. For that matter, I hope your SFCs use pseudo-namespacing for your JS, as well.
Ideally, there should be an interface standard for SFCs, as per GraphQL. However, GraphQL has no need for state/event changes.

Collapse
 
ryansolid profile image
Ryan Carniato • Edited

SolidJS is a different UI Framework that while it looks similar to React uses granular reactivity instead of VDOM to handle DOM updates. It's a competitor the libraries mentioned rather than something like a state management tool. It's more similar to taking Vue's composition API and building a full render library from it, (although it predates it by several years). React introducing tuple returns with hooks solved a longstanding API issue I was having so I adopted it. But Solid being based completely on those primitives independent of the Component system has unlimited composability, which I was trying to highlight at the end of the document.

CSS is sort of neither here or there IMHO. The bundler can handle that with CSS Modules or PostCSS, or CSS in JS solutions work as well. You can apply most CSS solutions outside of the framework or not and leave them global regardless of the your choice of framework.

The key take away from my perspective is about the rigidity of most SFC formats compared to a library like React, and even more so compared to a library like Solid. I'm advocating for patterns that organize code to match developer cognitive limits with the ability to extract portions easily as requirements grow. It's a more organic way of looking at component boundaries than firmly placing them for mechanical reasons for change management like React or syntactical limitations due to their respective SFC formats.

Collapse
 
abhinavanshul profile image
abhinav anshul

solid post

Collapse
 
garkey profile image
garkey

I arrived at your article seeking a way to 'wrap' a Svelte component in a function to abstract out similar components using svelte:component. To me, it's more reassuring to use native function and manipulate lambda abstractions until it absolutely must be rendered to a view. Once the data gets composed with declarative svelte-specific logic and syntax, that data + view is locked into svelte. Thanks for introducing me to SolidJS.