Nayaab Khan
Posted on April 12, 2022
⚠️ This is just an experiment that is clearly not meant for production.
After playing with SwiftUI for a few days I was impressed by its syntax and
wanted to see if I can write React this way. Possibly an absurd thought, but why not.
It turned out like this:
const SandwichItem = (sandwich: Sandwich) =>
HStack(
Image(sandwich.thumbnailName)
.rounded(8),
VStack(
Text(sandwich.name)
.as('h2')
.variant('title'),
Text(`${sandwich.ingredientCount} ingredients`)
.as('small')
.variant('subtitle'),
),
)
.alignment('center')
.spacing('medium')
render(root, List(sandwiches, SandwichItem))
To compare, this is what it would look like in JSX:
const SandwichItem = ({ sandwich }: SandwichItemProps) => (
<HStack alignment="center" spacing="medium">
<Image src={sandwich.thumbnailName} cornerRadius={8} />
<VStack>
<Text as="h2" variant="title">
{sandwich.name}
</Text>
<Text as="small" variant="subtitle">
{sandwich.ingredientCount} ingredients
</Text>
</VStack>
</HStack>
)
render(
root,
<List
items={sandwiches}
renderItem={(sandwich) => <SandwichItem sandwich={sandwich} />}
/>,
)
Syntax is subjective. I’m sure some of you will prefer one over the other for
various reasons. Besides personal preferences, a few things do stand out with this syntax:
- It is a composition of function calls, nothing on top of JavaScript.
- Components take required inputs as arguments.
- Modifications are done through chained-functions, called modifiers.
I particularly like the separation between inputs and modifiers. In JSX, both would be props.
Text('Professional photographer')
.variant('subtitle')
.color('muted')
Inputs could be primitive types like a string
or object types like an array of components.
VStack(
Text('Your Messages').variant('title'),
Text(`${unread} messages`).variant('subtitle'),
)
Polymorphic, Chain-able Modifiers
Modifiers are just functions. What makes them interesting is that they can be shared by multiple components and implemented independently.
Text(/*...*/)
.color('muted')
HStack(/*...*/)
.color('muted')
.align('start')
.spacing('gutter')
AutoGrid(/*...*/)
.minWidth(360)
.spacing('small')
Let’s see a couple of modifier implementations:
// Colorable.ts
export interface Colorable extends JSX.Element {
color: <T extends Colorable>(this: T, color: string) => T
}
export function color<T extends Colorable>(this: T, color: string) {
const style = {
...this.props.style,
color,
}
this.props = {
...this.props,
style,
}
return this
}
// Fontable.ts
type Font = keyof typeof fontVariants
export interface Fontable extends JSX.Element {
font: <T extends Fontable>(this: T, font: Font) => T
}
export function font<T extends Fontable>(this: T, font: Font) {
const style = {
...this.props.style,
...fontVariants[font],
}
this.props = {
...this.props,
style,
}
return this
}
A component can now implement these traits:
// Text.tsx
import { Fontable, font } from '@modifiers/Fontable'
import { Colorable, color } from '@modifiers/Colorable'
export default function Text(text: string): Fontable & Colorable {
const element = <span>{text}</span>
return {
...element,
font,
color,
}
}
And now the component could be invoked using:
Text('Hello, world!')
.font('title')
.color('hotpink')
A component can implement multiple modifiers which can be chained. Also, a modifier could be implemented by many components, making them polymorphic.
ℹ️ You can see more components and modifiers at https://github.com/nayaabkhan/swift-react.
Problems
There are a few things I noticed that don’t work. Very likely, there are be more.
-
useContext doesn’t work. But
<Context.Consumer />
works fine. - React Developer Tools doesn’t show the components in the inspector.
I suspect this is because React cannot identify our components as we don’t use either JSX or createElement
when creating them. If you find more problems, please report them in the comments. And if you know work-arounds, they’re very welcome.
But, Why?
Finally, let’s address the elephant in the room. Why all this trouble?
Maybe I’m bike shedding. But experimenting and sharing with everyone is the only way to know. Maybe this resonates with others and becomes a thing. Or gets buried in the rubble of bad ideas. Who knows, but it was worth it for the fun I had.
I had a great time authoring React this way. Maybe this kind of syntactic change can have unexpected, but useful impact.
- Allow component API designers to be very specific of expected input types. For instance,
Text
only accepts strings or markdown instead of any kind of ReactNode. - Make it easier to share the common API along with its implementation using modifiers.
- Introduce higher-level constructs like Identifiers.
- Swap React with something else without any impact on the users of the library.
In closing, I hope you try it out on CodeSandbox, have fun, and share your thoughts.
Until next time 👋.
Posted on April 12, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.