reacty_yew: Generating Yew components from React components via Typescript type definitions

For the people that are really thirsty for the code click here:

What it is

A project (for now mostly a proof of concept) to explore the possibility of generating Yew components based on the Typescript type definitions of React components.

What it isn't

It's definitely not intended for production use! Expect ugly crashed and unhelpful error messages!

In the current state, it only has very bare support regarding the types you can use in props for a component (string, number and any as an escape hatch). No support for child elements. It will also only recognize functional React components (and probably not even all variants of those).

Why I wrote it

I've always been a big fan of React and its JSX syntax (an opinion which might be unpopular in Rust circles). So naturally I gravitated towards Rust frontend frameworks that aim to provide a similar syntax, and was very happy once Yew emerged as a viable option for that. Trying to marry the two, I toyed around a little bit and was able to come up with an example on how to render a React component from inside a Yew component (which I also try to keep up to date with the latest versions of Yew).

More recently, I've been writing a Yew/React hybrid frontend for a private project of mine. I could have gone for a pure React or a pure Yew frontend, but decided to go with the hybrid for a few reasons:

  • The React ecosystem is huge and I knew of a few quality components that I wanted to take advandtage of in the project
  • The project involves a fair amount of byte-level handeling/conversion of data, where I feel like Rust (WASM) libraries are much easier to use
  • Pure Yew just isn't feasible right now (for me) in terms of iteration (compilation) speed, though the situation has recently improved with trunk and fully transitioning from stdweb to web_sys
  • Pure React (even with Typescript) doesn't provide me with the type safety I've gotten used to in Rust

While the hybrid approach has been mostly working great for me so far, every time I had to touch the Yew<->React boundary felt like a big chore! Every change to the props involves writing a bunch of boilerplate code and changes in 4-5 different places with ample opportunities for mistakes.

I've carried around the idea for this project for quite some time now, but every time I tried to extract some useful type information out of Typescript type declarations in the past, I ran into a wall. Luckily I stumbled upon the right part of their Compiler API docs this time!

Why you might care

As I've already mentioned, the React ecosystem is huge!

While native frameworks like ybc (Bulma-based) are starting to emerge and are likely preferable to wrapping components from foreign frameworks, it will take a long long time until those can realistically rival the existing ones in breadth and polish.

Personally, I think libraries like(!) this one that allow the Rust frontend ecostystem to piggyback off of the efforts that went into the existing JS frontend world will be crucial to it's success, but obviously I'm biased here. 🙃

What it looks like

For the full example including project structure see the bad_button example.

Given a React component:

import * as React from "react"; interface BadButtonProps { color?: string, text: string, } const BadButton = (props: BadButtonProps): JSX.Element => { return ( <button style={{ backgroundColor: props.color }} onClick={() => window.alert("Click!")} > {props.text} </button> ); }; export { BadButton };

And an inclusion of the UMD package of your JS library:

<html lang="en"> <head> <script crossorigin src="/bad_button_lib.umd.js"></script> </head> </html>

An invocation of the react_component_mod! macro (takes as input the name of the module to generate, path to the type declarations and the JS global (UMD) that holds the React components) generates a module:

react_component_mod!( bad_button; types = "../js_package/dist/index.d.ts", global = "BadButtonLib" );

You can then directly use the generated component BadButton in a Yew component:

use yew::prelude::*; use crate::bad_button::BadButton; // ... fn view(&self) -> Html { html! { <div> <BadButton text="Actual props" /> </div> } } // ...

Minimal boilerplate! Yay!🎉

What it looks like internally

A few glimpses into the internal workings:

  • The project mainly consists of two main parts: The "analyzer" script and the react_component_mod proc macro
  • The analyzer script is written in Typescript, and compiled to Javascript
  • The JS version of the script is inlined to the Rust source and will be written to the Rust target directory during invocation
  • The proc macro is provided with the path to a Typescript declaration entry point
  • The analyzer script is run via a node process (very rust-analyzer/IDE unfriendly!) to get all the type/component information from the type declaration file
  • The output of the analyzer is then used to generate a Rust module containing: structs for props, structs for components, yew::Component impl for the components, conversion between Rust type and wasm-bindgen/JS types, generation of inline JS scripts that take the props and call ReactDOM.render

What the future of the project looks like

Who knows if anyone will even read this blog post, or there is any interest in the project 🤷

First and foremost I'm interested in a project like this existing!

To be completely upfront I'm probably not the right person to maintain this project (alone), as my academic/work commitments regularly render me unavailable. So if you are interested in (co-)maintining the project feel free to reach out on the issue tracker! If you look at the existing implementation in horror/disgust and think it's more worthwile to start an alternative from scratch, go for it!