DEV Community

Cover image for Swift-like React: Can We? Should We?
Nayaab Khan
Nayaab Khan

Posted on • Edited on

Swift-like React: Can We? Should We?

⚠️ 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))
Enter fullscreen mode Exit fullscreen mode

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} />}
  />,
)
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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'),
)
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode
// 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
}
Enter fullscreen mode Exit fullscreen mode

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,
  }
}
Enter fullscreen mode Exit fullscreen mode

And now the component could be invoked using:

Text('Hello, world!')
  .font('title')
  .color('hotpink')
Enter fullscreen mode Exit fullscreen mode

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 👋.

Top comments (0)