React Single File Components Are Here

The launch of RedwoodJS today marks a first: it is the first time React components are being expressed in a single file format with explicit conventions.

I first talked about this in relation to my post on React Distros, but I think React SFCs will reach standardization in the near future and it is worth discussing now as a standalone post.

Context 🔗 It is a common joke that you can't get through a React conference without reference to React as making your view layer a pure function of data, v = f(d) . I've talked before about the problems with this, but basically React has always done a collective ¯\_(ツ)_/¯ when it comes to having a satisfying solution for actually fetching that all-important data. React Suspense for Data Fetching may be a nice solution for this in the near future. Here's a typical React "single file component" with Apollo Client, though it is the same with React Query or any data fetching paradigm I can imagine: // Data Example 1 export const QUERY = gql ` query { posts { id title body createdAt } } ` ; export default function MyComponent () { const { loading , error , data : posts } = useQuery (QUERY) ; if ( error ) return < div >Error loading posts: { error . message }</ div > if ( loading ) return < div >Loading...</ div >; if ( ! posts . length) return < div >No posts yet!</ div >; return ( <> posts . map ( post => ( < article > < h2 >{ post .title}</ h2 > < div >{ post .body}</ div > </ article > )); </> ) } For styling, we might use anything from Tailwind to Styled-JSX to Styled-Components/Emotion to Linaria/Astroturf to CSS Modules/PostCSS/SASS/etc and it is a confusing exhausting random eclectic mix of stuff that makes many experts happy and many beginners lost. But we'll talk styling later.

Redwood Cells 🔗 Here's what Redwood Cells look like: // Data Example 2 export const QUERY = gql ` query { posts { id title body createdAt } } ` ; export const Loading = () => < div >Loading...</ div >; export const Empty = () => < div >No posts yet!</ div >; export const Failure = ({ error }) => < div >Error loading posts: { error . message }</ div >; export const Success = ({ posts }) => { return posts . map ( post => ( < article > < h2 >{ post .title}</ h2 > < div >{ post .body}</ div > </ article > )) ; }; This does the same thing as the "SFC" example above, except instead of writing a bunch of if statements, we are baking in some conventions to make things more declarative. Notice in both examples are already breaking out the GraphQL queries, so that they can be statically consumed by the Relay Compiler, for example, for persisted queries. This is a format that is more native to React's paradigms than the Single File Component formats of Vue, Svelte, and my own React SFC proposal a year ago, and I like it a lot. Notice that, unlike the HTML-inspired formats of the above options, we don't actually lose the ability to declare smaller utility components within the same file, since we can just use them inline without exporting them. This has been a major objection of React devs for SFCs in the past.

Why Formats over Functions are better 🔗 As you can see, the authoring experience between the Example 1 and Example 2 is rather nuanced, and to articulate this better, I have started calling this idea Formats over Functions. The idea is that you don't actually need to evaluate the entire component's code and mock the data in order to access one little thing. In this way you bet on JS more than you bet on React itself. This makes your components more consumable and statically analyzable by different toolchains, for example, by Storybook.

Component Story Format 🔗 Team Storybook was actually first to this idea and a major inspiration for me, with it's Component Story Format. Here's how stories used to be written: // Storybook Example 1 import React from ' react ' ; import { storiesOf } from ' @storybook/react ' ; import { Button } from ' @storybook/react/demo ' ; storiesOf ( ' Button ' , module ) . addWithJSX ( ' with Text ' , () => < Button >Hello Button</ Button > ) . add ( ' with Emoji ' , () => ( < Button > < span role= " img " aria-label= " so cool " > 😀 😎 👍 💯 </ span > </ Button > )) ; You needed to have Storybook installed to make use of this, and if you ever needed to migrate off Storybook to a competitor, or to reuse these components for tests (I have been in this exact scenario), you were kind of screwed. Component Story Format (CSF) lets you write your components like this: // Storybook Example 2 import React from ' react ' ; import { Button } from ' @storybook/react/demo ' ; export default { title : ' Button ' }; export const withText = () => < Button >Hello Button</ Button >; export const withEmoji = () => ( < Button > < span role= " img " aria-label= " so cool " > 😀 😎 👍 💯 </ span > </ Button > ) ; and all of a sudden you can consume these files in a lot more different ways by different toolchains (including by design tools!) and none of them have to use Storybook's code, because all they need to know is the spec of the format and how to parse JavaScript. (yes, JSX compiles to React.createElement, but that is easily mockable).

Merging CSF and SFCs 🔗 You're probably already seeing the similarities - why are we authoring stories and components separately? Let's just stick them together? You already can: // CSF + SFC Example export default { title : ' PostList ' , excludeStories : [ ' QUERY ' ] }; export const QUERY = gql ` query { posts { id title body createdAt } } ` ; export const Loading = () => < div >Loading...</ div >; export const Empty = () => < div >No posts yet!</ div >; export const Failure = ({ error }) => < div >Error loading posts: { error . message }</ div >; export const Success = ({ posts }) => { return posts . map ( post => ( < article > < h2 >{ post .title}</ h2 > < div >{ post .body}</ div > </ article > )) ; }; Adding TypeScript would take no change in tooling at all. And therein lies the beauty of the format, and the impending necessity of standardizing exports for fear of stepping on each others' toes as we push forward React developer experience.

The Full Potential of Single File Components 🔗 I think Styling is the last major frontier we need to integrate. Utility CSS approaches aside, here's how we can include static scoped styles for our components: // Styled SFC - Static Example export const STYLE = ` /* only applies to this component */ h2 { color: red } ` export const Success = ({ posts }) => { return posts . map ( post => ( < article > < h2 >{ post .title}</ h2 > < div >{ post .body}</ div > </ article > )) ; }; // etc... and, if we needed dynamic styles, the upgrade path would be fairly simple: // Styled SFC - Dynamic Example export const STYLE = props => ` h2 { color: ${ props . color } // dynamic! } ` // etc... And that would upgrade to a CSS-in-JS equivalent implementation.

Interacting with Hooks 🔗 What if styles or other future Single File Component segments need to interact with component state? We could lift hooks up to the module level: // Hooks SFC Example const [ toggle , setToggle ] = useState ( false ) export const STYLE = ` h2 { color: ${ toggle ? " red " : " blue "} } ` export const Success = ({ posts }) => { return posts . map ( post => ( < article > < h2 onClick= {() => setToggle ( ! toggle )}>{ post .title}</ h2 > { toggle && < div >{ post .body}</ div >} </ article > )) ; }; This of course changes the degree of reliance on the React runtime that we assume in SFCs, so I am less confident about this idea, but I do still think it would be useful. I have other, more extreme ideas on this front.

Other Opportunities 🔗 Dan Abramov replied with something I missed - the server/client split. There is ongoing work with React Flight (to do with streaming SSR) and Blocks (to do with blocking rendering without being tied to Relay/GraphQL) that I'm basically completely ignorant about. While Redwood uses exports to declare loading and error states, Suspense uses <Suspense> and error boundaries. It's possible to compile from the former to the latter but not the other way, which is a key point of the "formats over functions" idea - things are more consumable that way. However it is a valid question whether you should be able to access those internal states - after all, if you're just reading them for testing purposes, should you be testing how React works? Counterpoint: what if you wanted to see your loading and error states separately in a Storybook or design tool? It also brings to mind the work that Next.js has done with getStaticProps , getStaticPaths and getServerSideProps - as the first hybrid framework, it is nice to use static exports to let the framework pick from data requirements, as well as to not tie yourself so tightly to GraphQL. getStaticPaths in particular is very elegant - moving page creation inside components themselves.

Conclusion - Ending with Why 🔗 It's reasonable to question why we want everything-in-one file rather than everything-in-a-folder. But in a sense, SFCs simply centralize what we already do with loaders. Think about it: we often operate like this: /components/myComponent/Component.tsx /components/myComponent/Component.scss /components/myComponent/Component.graphql /components/myComponent/Component.stories.js /components/myComponent/Component.test.js And some people may think that is better than this: /components/myComponent/Component.tsx /styles/Component.scss /graphql/Component.graphql /stories/myComponent/Component.stories.js /tests/myComponent/Component.test.js But we're exchanging that for: export default = // ... metadata export const STYLE = // ... export const QUERY = // ... export const Success = // ... export const Stories = // ... export const Test = // ... I find that the file length is mitigated by having keyboard shortcuts for folding/expanding code in IDE's. In VSCode, you can fold/unfold code with keyboard bindings: Fold folds the innermost uncollapsed region at the cursor: Ctrl + Shift + [ on Windows and Linux

on Windows and Linux ⌥ + ⌘ + [ on macOS Unfold unfolds the collapsed region at the cursor: Ctrl + Shift + ] on Windows and Linux

on Windows and Linux ⌥ + ⌘ + ] on macOS Fold All folds all regions in the editor: Ctrl + (K => 0) (zero) on Windows and Linux

(zero) on Windows and Linux ⌘ + (K => 0) (zero) on macOS Unfold All unfolds all regions in the editor: Ctrl + (K => J) on Windows and Linux

on Windows and Linux ⌘ + (K => J) on macOS Ultimately, Colocating concerns rather than artificially separating them helps us delete and move them around easier, and that optimizes for change. I have more thoughts on how we can apply twists of this ideahere on my old proposal. This movement has been a long time coming, and I can see the momentum accelerating now.