Using Fstyle with Vue and React

2023-10-14
James Diacono
james@diacono.com.au

Styling web applications is hard. CSS is a poor fit for application development, but the alternatives are severely limited. In practice, we are forced to depend on some kind of style system to manage our styles.

Here are four things I want from a style system:

  1. Control. It should give me the full power of CSS. A subset will not suffice.
  2. Dynamic. It should be easy to make styles vary in real time. There should not be separate systems for static and dynamic styles.
  3. Buildless. Fast feedback during development is vital. Configuring, maintaining and enduring build steps is the bane of my existence.
  4. Modular. I want to define styles as modules, and compose them together.

I could not find a tool that satisfied all of these requirements, so I wrote Fstyle.


If you are a web developer, Vue and React probably require no introduction. At the time of writing, both are wildly popular JavaScript frameworks for building web applications in the reactive paradigm. Superficially, they seem very different. But deep down they are almost the same. I will show how Fstyle can be used with either of these frameworks, and in doing so I hope to reveal something about their inner nature.

Immediately below this paragraph you should see an animated progress bar. You can simulate the amount of progress by dragging the slider beneath it. The links in the heading control whether it is rendered with Vue or React.

The component's implementation presents two challenges. Firstly, it is obviously dynamic. Its appearance changes in response to events. Secondly, it uses a CSS animation. This means that manipulating the style property of its DOM elements will not work. The full power of CSS is required.

The source code of the component is listed below. Code in this colour is used only by the Vue implementation, whereas code in this colour is used only by the React implementation. Code in this colour is used by both.

We begin by importing the necessary modules. There are some interesting equivalences. React's createElement is so similar to Vue's h function that we rename it h. The ref function serves the same purpose as the useState function, likewise createApp and render.


import fstyle from "https://james.diacono.com.au/files/fstyle.js";
import {
    h,
    ref,
    readonly,
    watchEffect,
    onUnmounted,
    createApp
} from "https://esm.sh/vue@3";
import {
    createElement as h,
    useState,
    useEffect,
    useRef
} from "https://esm.sh/react@17";
import {render} from "https://esm.sh/react-dom@17";

We define some stylers. Each styler is used to style an element in the component. demo_styler positions the progress bar, the slider, and the text within the containing element. Each [] is replaced by the generated class f0⎧demo⎭ in the resulting CSS.


const demo_styler = fstyle.css(function demo() {
    return `
        .[] {
            display: flex;
            flex-direction: column;
            align-items: center;
        }
        .[] > :not(:last-child) {
            margin-bottom: 20px;
        }
    `;
});

The progress_bar styler styles the progress bar, consisting of a parent and a child element. Supposing the percentage parameter was 50, then the child element would fill the left half of the parent and [] would be replaced with the generated class f1⎧progress_bar⎭·percentage→50.


const progress_bar_styler = fstyle.css(function progress_bar({percentage}) {
    return `
        .[] {
            display: block;
            overflow: hidden;
            border-radius: 10px;
            width: 80%;
            box-shadow: 0px 5px 15px rgba(0, 0, 0, 0.3);
            background: white;
        }
        .[] > * {
            display: block;
            overflow: hidden;
            width: ${percentage}%;
            height: 40px;
        }
    `;
});

The stripes styler gives an element an animated stripey background. The thickness of the stripes, their colour, and the duration of the animation cycle are all provided as parameters.


const stripes_styler = fstyle.css(function stripes({
    scale,
    primary_color,
    secondary_color,
    duration
}) {
    return `
        @keyframes [] {
            from {
                background-position-x: ${scale};
            }
            to {
                background-position-x: 0;
            }
        }
        .[] {
            --thickness: calc(${scale} / 2.8282); /* 2√2 */
            background-color: ${secondary_color};
            background-image: repeating-linear-gradient(
                45deg,
                transparent,
                transparent var(--thickness),
                ${primary_color} var(--thickness),
                ${primary_color} calc(2 * var(--thickness))
            );
            background-size: ${scale} 100%;
            animation: [] ${duration} linear infinite;
        }
    `;
});

We create an Fstyle context to manage the injection of CSS onto the page.


const context = fstyle.context();

The use_fstyle function is a Vue composable that serves as the glue between Fstyle and Vue. It is passed a styler function that is made available for the lifetime of the component. It returns a ref containing an array of class strings to be applied to an element. (A ref is an object with a reactive “value” property.) The styler function is invoked within watcher so that any reactive side effects, such as the reading of props, cause the handle to be recreated.


function use_fstyle(styler) {
    let classes = ref([]);
    let handle;
    watchEffect(function watcher() {
        const requireable = styler();
        const new_handle = context.require(requireable);
        if (handle !== undefined) {
            handle.release();
        }
        classes.value = new_handle.classes;
        handle = new_handle;
    });
    onUnmounted(function () {
        handle.release();
    });
    return readonly(classes);
}

The use_fstyle function is a React hook. It is like the use_fstyle composable except for two important differences:

Some unreactive state, equivalent to the handle variable, is maintained via useRef.


function use_fstyle(requireable) {
    const new_handle = context.require(requireable);
    const handle_ref = useRef();
    if (handle_ref.current !== undefined) {
        handle_ref.current.release();
    }
    handle_ref.current = new_handle;
    useEffect(
        function on_mount() {
            return function on_unmount() {
                handle_ref.current.release();
            };
        },
        []
    );
    return new_handle.classes.join(" ");
}

Now we begin the component function, defining our component. The progress, initially 75%, is stored as a percentage. In Vue, this number is held in a percentage ref. React uses a percentage number and a set_percentage function to represent the value reactively.


function component() {
    const percentage = ref(75);
    const [percentage, set_percentage] = useState(75);

We make and “use” the necessary stylers. In Vue, the percentage ref must be unwrapped. In React, percentage is a number and can be used directly.

In React, each styler is called immediately to obtain a requireable. In Vue, the call is wrapped in a function that will be sensitive to any reactive effects within. In this case, any change to percentage.value will cause the handle to be recreated.


    const demo_classes = use_fstyle(() => demo_styler());
    const bar_classes = use_fstyle(() => progress_bar_styler({
        percentage: percentage.value
        percentage
    }));
    const filling_classes = use_fstyle(() => stripes_styler({
        scale: "130px",
        duration: "1s",
        primary_color: "yellow",
        secondary_color: "gray"
    }));

During each render, the relevant classes are applied to the <progress_bar> element and its child, <progress_filling>. The h function returns an object (called a “React element” in React and a “vnode” in Vue) describing the desired state of the DOM. We have invented our own element names, "progress_bar" and "progress_filling", to improve readability.

In Vue, the render_progress_bar function is reactive to any change in bar_classes.value or filling_classes.value.


    function render_progress_bar() {
        return h(
            "progress_bar",
            {class: bar_classes.value},
            {class: bar_classes},
            h(
                "progress_filling",
                {class: filling_classes.value}
                {class: filling_classes}
            )
        );
    }

Adjusting the slider updates the percentage.


    function render_slider() {
        return h(
            "input",
            {
                type: "range",
                min: 0,
                max: 100,
                value: percentage.value,
                value: percentage,
                onInput(event) {
                    const new_percentage = parseInt(event.target.value, 10);
                    percentage.value = new_percentage;
                    set_percentage(new_percentage);
                }
            }
        );
    }
    function render_demo() {
        return h(
            "div",
            {class: demo_classes.value},
            {class: demo_classes},
            [
                render_progress_bar(),
                render_slider(),
                h("small", {}, "Rendered with Vue.")
                h("small", {}, "Rendered with React.")
            ]
        );
    };

Here is the crucial difference between Vue and React. In Vue, the component function is called only when the component is created. A render function is returned, and is invoked whenever the component needs to be rerendered. On the other hand, component (and hence render_demo) is called for every render in React.

In short, Vue takes advantage of closures whereas React does not.


    return render_demo;
    return render_demo();
}

Now that the component function has been defined, we hand it off to the framework for rendering.


const element = document.querySelector("#demo");
createApp({setup: component}).mount(element);
render(h(component), element);