react-sfc
v0.1.1
Published
Swyx's proposal for bringing Single File Components to React. [Other proposals can be found here](https://github.com/react-sfc/react-sfc-proposal).
Downloads
17
Readme
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
orrsfc 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 oncereact-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
- Plugin: https://github.com/sw-yx/rollup-plugin-react-sfc
- Demo: https://github.com/sw-yx/rollup-react-boilerplate
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
- In 1 image
- Features implemented
- Basic Proposal
- Advanced Opportunities
- Why? I don't need this!
- General principle: Loaders vs SFCs
- Am I missing some obvious idea or some critical flaw?
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
Features implemented
- [x] Automatic react import
- [x] mutable useState
_
syntax - [x] useStateWithLabel hook replaces useState to label in prod
- [x] Dynamic CSS transform to styled-JSX
- [x] set displayName if passed as compiler option
- [x]
$value={$text}
binding for onChange- this works for nested properties eg
$value={$text.foo}
- this works for nested properties eg
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
- considered
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>
}
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.