react-sfc
TypeScript icon, indicating that this package has built-in type declarations

0.1.1 • Public • Published

Experimental React Single File Components

Swyx's Experimental Proposal for bringing Single File Components to React. Other proposals can be found here. The specific APIs are unstable for now and have already changed from what was shown at the React Rally talk!

This is an experiment as a proof of concept and will just be a toy unless other folks pick it up/help contribute/design/maintain it! Let me know what your interest is and help spread the word.

Usage

2 ways use React SFCs in your app:

As a CLI

To gradually adopt this in pre-existing React projects - you can leave your project exactly as is and only write individual SFCs in a separate folder, without touching your bundler config at all.

  • npm i react-sfc
  • Create a /react origin folder in your project to watch and compile from.
  • We assume you have a destination /src output folder with the rest of your app, to compile to.
  • run react-sfc watch or rsfc watch.
  • Now you are free to create /react/MyButton.react files in that folder

CLI Flags:

  • If you need to customize the names of the folders that you are compiling from and compiling to, you can pass CLI flags: react-sfc watch -f MYORIGINFOLDER -t MYOUTPUTFOLDER
  • By default, the CLI compiles .react files into .js files. If you need it to output .tsx files or other, you can pass the extension flag --extension tsx or -e tsx. Note: the developer experience for this is not yet tested.

Other commands:

  • if you don't need a watch workflow, you can also do single runs with other commands (same CLI flags apply):
    • react-sfc build to build once
    • react-sfc validate to parse your origin folder without building, to check for errors

As a Rollup plugin

In a new or pre-existing React + Rollup project

Other ways

TBD. need help to write a webpack plugin version of this.


Special note to readers: this package is deployed to react-sfc on npm right now - but i am not going to be selfish at all about this. if someone else comes along with a better impl i will give you the npm name and github org. Please come and take it.

Table of Contents

Table of Contents

Design Goals

  • Stay "Close to JavaScript" to benefit from existing tooling: syntax highlighting, autocomplete/autoimport, static exports, TypeScript
  • Have easy upgrade paths to go from a basic component to dynamic styles, or add state, or extract graphql dependencies
  • Reduce verbosity without sacrificing readability

This probably means that a successful React SFC should be a superset of normal React: you should be able to rename any .js and .jsx file and it should "just work", before taking advantage of any new features.

In 1 image

image

Features implemented

  • Automatic react import
  • mutable useState _ syntax
  • useStateWithLabel hook replaces useState to label in prod
  • Dynamic CSS transform to styled-JSX
  • set displayName if passed as compiler option
  • $value={$text} binding for onChange
    • this works for nested properties eg $value={$text.foo}

TODO:

  • JS and CSS sourcemaps
  • it does not properly work with styled-jsx in rollup - need SUPER hacky shit to work (see boilerplate's index.html)
  • useEffect dependency tracking
  • automatically extract text for i18n
  • nothing graphql related yet
  • optional css no-op function for syntax highlighting in JS
  • $value shorthand eg $value
  • $value generalized eg $style
  • handle multiple bindings
  • test for TSX support?

open questions

  • what binding syntax is best?
    • considered bind:value but typescript does not like that
    • $ prefix works but doesnt look coherent with the rest of RSFC format. using this for now
    • _ prefix looks ugly? <- went with this one

Basic Proposal

Here is how we might write a React Single File Component:

let _count = 1
 
export const STYLE = `
    div { /* scoped by default */
      background-color: ${_count > 4 ? "papayawhip" : "palegoldenrod"};
    }
  `
 
export default () => {
  useEffect(() => console.log('rerendered'))
  return (
    <button onClick={() => _count++}> 
      Counter {_count}
    </button>
  )
}

The component name would be taken from the filename. Named exports would also be externally accessible.

Advanced Opportunities

These require more work done by the surrounding compiler/distribution, and offer a lot of room for innovation:

CSS in JS

We can switch nicely from no-runtime scoped styles to CSS-in-JS:

export const STYLE = props => `
    div {
      background-color: ${props.bgColor || 'papayawhip'};
    }
  `
// etc

In future we might offer a no-op css function that would make it easier for editor tooling to do CSS in JS syntax highlighting:

export const STYLE = css`
    div { /* properly syntax highlighted */
      background-color: blue;
    }
`

State

We can declare mutable state:

let _count = 0
 
export const STYLE = `
    button {
      // scoped by default
      background-color: ${_count > 5 ? 'red' : 'papayawhip'};
    }
  `
 
export default () => {
  return <button onClick={() => _count++}>Click {_count}</button>
}
and this is transformed to the appropriate React APIs.
export default const FILENAME = () => {
  const [_count, set_Count] = useState(0);
  return (
    <>
      <button onClick={() => set_Count(_count++)}>Click {_count}</button>
      <style jsx>
        {`
          button {
            // scoped by default
            background-color: ${_count > 5 ? "red" : "papayawhip"};
          }
        `}
      </style>
    </>
  );
};

We can also do local two way binding to make forms a lot easier:

let data = {
  firstName: '',
  lastName: '',
  age: undefined,
}
 
function onSubmit(event) {
  event.preventDefault()
  fetch('/myendpoint, {
    method: 'POST',
    body: JSON.stringify(data)
  })
}
 
export default () => {
  return (
    <form onSubmit={onSubmit}>
      <label>
        First Name
        <input type="text" bind:value={data.firstName} />
      </label>
      <label>
        Last Name
        <input type="text" bind:value={data.lastName} />
      </label>
      <label>
        Age
        <input type="number" bind:value={data.age} />
      </label>
      <button type="submit">Submit</button>
    </form>
  )
}

Binding

Local two way binding can be really nice.

let $text = 0
 
export default () => {
  return <input $value={$text} />
}

And this transpiles to the appropriate onChange handler and value attribute. It would also have to handle object access.

Another feature from Vue and Svelte that is handy is class binding. JSX only offers className as a string. We could do better:

let _foo = 0
let _bar = 0
 
export default () => {
  return <form>
    <span $class={{
      class1: _foo,
      class2: _bar,
    }}>Test<span>
    <button onClick={() => _foo++}> Click {_foo}</button>
    <button onClick={() => _bar++}> Click {_bar}</button>
  </form>
}

GraphQL

The future of React is Render-as-you-Fetch data, and being able to statically extract the data dependencies from the component (without rendering it) is important to avoid Data waterfalls:

export const GRAPHQL = `
    query MYPOSTS {
      posts {
        title
        author
      }
    }
  `
 
export default function MYFILE (props, {data, status}) {
    if (typeof status === Error) return <div>Error {data.state.message}</div>
    return (
      <div>
        Posts:
        {status.isLoading() ? <div> Loading... </div>
        : (
          <ul>
            {data.map((item, i) => <li key={i}>{item}</li>)}
          </ul>
        )
        }
      </div>
    )
  }
}

Dev Optimizations

We can offer other compile time optimizations for React:

  • Named State Hooks

Automatically insert useDebugValue for each useState:

function useStateWithLabel(initialValue, name) {
    const [value, setValue] = useState(initialValue);
    useDebugValue(`${name}${value}`);
    return [value, setValue];
}
  • Auto optimized useEffect

Automatically insert all dependencies when using useAutoEffect, exactly similar to https://github.com/yuchi/hooks.macro

Why? I don't need this!

That's right, you don't -need- it. SFCs are always sugar, just like JSX. You don't need it, but when it is enough of a community standard it makes things nicer for almost everyone. SFC's aren't a required part of Vue, but they are a welcome community norm.

The goal isn't to evaluate this idea based on need. In my mind this will live or die based on how well it accomplishes two goals:

  • For beginners, provide a blessed structure in a chaotic world of anything-goes.
  • For experts, provide a nicer DX by encoding extremely common boilerplatey patterns in syntax.

Any new file format starts with a handicap of not working with existing tooling e.g. syntax highlighting. So a successful React SFC effort will also need to have a plan for critical tooling.

General principle: Loaders vs SFCs

Stepping back from concrete examples to discuss how this might affect DX. In a sense, SFCs simply centralize what we already do with loaders. Instead of

Component.jsx
Component.scss
Component.graphql

we have

export const STYLE // etc 
export const GRAPHQL // etc 
export default () => <div /> // etc

in a file. Why would we exchange file separation for a super long file? Although there are ways to mitigate this, it is not very appealing on its own.

However, to the extent that the React SFC loader is a single entry point to webpack for all these different filetypes, we have the opportunity to simplify config, skip small amounts of boilerplate, and enforce some consistency with the single file format. Having fewer files causes less pollution of IDE file namespace, and makes it easier to set up these peripheral concerns around jsx (styling, data, tests, documentation, etc) incrementally without messing with creating/deleting files.

Am I missing some obvious idea or some critical flaw?

File an issue or PR or tweet at me, lets chat.

Readme

Keywords

none

Package Sidebar

Install

npm i react-sfc

Weekly Downloads

1

Version

0.1.1

License

MIT

Unpacked Size

63.7 kB

Total Files

11

Last publish

Collaborators

  • sw-yx