Exploring SolidJS with Turborepo and Vanilla-extract

Find out how we created a Wordle game within a Turborepo with SolidJS and Vanilla-extract as the technology stack!
7 min read
Jonathan Gomand
Exploring SolidJS with Turborepo and Vanilla-extract

In our last growth session at Mirahi, we wanted to discover SolidJS, a declarative, efficient, and flexible JavaScript library for building user interfaces and compare it to React and Vue.

To take full advantage of this growth, we also investigated Turborepo and Vanilla-extract to see if it can improve our development phase and could be used in some of our clients, or internals projects.

The goal of the project was to create a Wordle game with SolidJS, compare it to a React and a Vue alternative.

To achieve that, we created a monorepo with Turborepo so that we can easily host the 2 examples and compare them with our SolidJS version of Wordle (and maybe add more Wordle games written with other frameworks/libraries in the future). We also created a shared styling library inside of our monorepo which is Vanilla-extract.

Turborepo

It is an Open Source project from Vercel and has at the time of writing a blogpost with 7.4k stars on GitHub.

Creating a monorepo with Turborepo is very simple, launch the following command, choose the name of your monorepo and the package manager to use, and you will have a preconfigured and ready to go monorepo:

npx create-turbo@latest

Our Turborepo files structure is inspired by Nx, which we often use in our projects at Mirahi and in our clients's projects.

Nx, like Turborepo, is a build system qualified as smart, fast and extensible. Among the large panel of features it offers, here is a quick look at the most interesting ones:

  • Smart rebuild, Nx will only rebuild what is affected by the code you edited.
  • Easy code sharing, split your code in different shared libraries that you can use in your apps.
  • Apps folder which contains your apps, in our case 3: different Wordle games.
  • Packages folder which contains your libraries to share with your apps, in our case we renamed it to "libs" and it contains the game settings for Wordle and the styles from Vanilla-extract. The others libraries are pre-installed when creating the Turborepo:
    - config: shared ESLint configuration
    - ui: shared React components
    - tsconfig: shared tsconfig configuration
  • turbo.json file which contains the configuration of Turborepo. You can specify pipelines inside this file to define the way your npm scripts relate to each other.
{
  "$schema": "https://turborepo.org/schema.json",
  "baseBranch": "origin/main",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      // note: output globs are relative to each package's `package.json`
      // (and not the monorepo root)
      "outputs": [".next/**"]
    },
    "test": {
      "dependsOn": ["^build"],
      "outputs": []
    },
    "lint": {
      "outputs": []
    },
    "dev": {
      "cache": false
    }
  }
}
  • package.json file to handle the dependencies, unlike Nx who store all the dependencies inside a single package.json file at the monorepo's root, Turborepo allows each app and each library to have their own package.json file, if you want to use a "ui" library in a "website" app for example, all you need to do is specify the library name with an "*" in the package.json file dependencies of the "website", and you will be able to import everything that is exported from that library:
  "dependencies": {
    "next": "12.0.8",
    "react": "17.0.2",
    "react-dom": "17.0.2",
    "react-helmet": "^6.1.0",
    "ui": "*"
  },

Vanilla-extract

This library allows to write CSS classes with TypeScript and then generate static CSS files at build time.

It is an Open Source project with at the time of writing this blogpost has 5.3k stars on GitHub.

The setup is easily done when working with popular bundlers like Webpack or Vite, find the setup guide here.

For our SolidJS project, we used this library to create our style, here is an example.

import { style } from '@vanilla-extract/css';

export const styledKeyboard = style({
  display: 'flex',
  flexDirection: 'column',
  alignItems: 'center',
});

Then you can use this constant in a class attribute.

import { styledKeyboard } from 'styles/components/keyboard.css';

export function Keyboard() {
  return(
    <div class={styledKeyboard}>
      // ...
    </div>
  );
};

You can do more than just writing your CSS rules with TypeScript:

  • Create themes where you can store configuration for your application, such as colours, fonts, etc...
  • Scoped CSS variables, the variables you will create inside a scope will only be available for the style in that specific scope and also the styles
  • And of course typechecking and code completion!

SolidJS

Now let's talk about SolidJS, I will show you comparisons with React, because you will see it looks quite the same. If you come from React, SolidJS should not be hard to understand.

It is an Open Source project since 2018 with at the time of writing this blogpost has 17k stars on GitHub.

Compared to React, SolidJS doesn't use a virtual DOM but works directly with the DOM. Also components don't re-render, only the data that has changed re-renders but not the entire component unlike React.

Those two particularities make SolidJS run faster and smoother than React.

For our Wordle project, we had to use logics like state management and lifecycles for our components.

State management

When in React, you use the useState hook to handle your component state, in SoldiJS you will use a createSignal reactive primitive.

// React state management
const [count, setCount] = useState(0);

// SolidJS state management
const [count, setCount] = createSignal(0);

In both cases you will invoke a function with an optional initial value (0 in our case) and this function will return a value and a function to set the value.

The main differences between the 2 examples above are the following:

  • In React, you can access count value directly by using the variable, in SolidJS this is a getter function so you will get the value like so: count()
  • In React, when the count value changes, the entire component re-renders, in SolidJS only the parts where count is used will re-render. This point is a plus in terms of performance.

Effects and component lifecycle

In React we can create effects with the useEffect hook. We can run effects only after the first render, after every render or only when a dependency value changes; so you basically handle the lifecycle of your components with only this hook.

In SolidJS we have a more "Vue-like" way to handle the effects and the lifecycle of our components, you have access to different functions like the following:

  • createEffect: run an effect whenever the dependency changes.
  • onMount: run only after the first render.
  • onCleanup: run when the component is destroyed or when the current reactive scope is recalculated.
// Takes a callback function as first argument and dependency as second argument.
createEffect(() => console.log("count as been updated!"), count);

// Takes a callback function that will on run at the first render.
onMount(() => console.log("This will only shows at the first render."))

// Takes a callback function that will run when the component is about to get destroyed.
onCleanup(() => console.log("Component is about to get destroyed!"))

Control flow

A good functionality of SolidJS is the control flow, the library provides us ready to use components to handle the flow of our components. For example in React when you have to loop over an array of values and display those values in the DOM, you would use a map function most of the time, in SolidJS you can use the <For> component.

// Your array of values, is ususally stored in a state.
const persons = ["Toto", "Tata", "Tutu"];

return(
  <For each={persons()} fallback={<div>Loading...</div>}>
    {(person) => <span>{person}</span>}
  </For>
)

With all of that in mind, here you can see a very basic component written with SolidJS. The following component is a list of persons handlers, it display a list of persons and a form to add people to the list, you can also delete a person by clicking on his name. It's a simple example of state management and control flow.

function Counter() {
  const [persons, setPersons] = createSignal([]);
  const [newPerson, setNewPerson] = createSignal("");

  const submitHandler = (e) => {
    e.preventDefault();
    setPersons([...persons(), newPerson()]);
    setNewPerson("");
  };

  const deleteHandler = (personToFind) => {
    const newPersons = persons().filter((person) => person !== personToFind);
    setPersons(newPersons);
  };

  return (
    <div>
      <div>
        <Show when={persons().length > 0} fallback={<span>List is empty</span>}>
          <For each={persons()} fallback={<div>Loading...</div>}>
            {(person) => (
              <span onClick={() => deleteHandler(person)}>{person}</span>
            )}
          </For>
        </Show>
      </div>
      <form onSubmit={submitHandler}>
        <label htmlFor="person">
          <input
            type="text"
            id="person"
            value={newPerson()}
            onChange={(e) => setNewPerson(e.target.value)}
          />
        </label>
        <button type="submit">Add</button>
      </form>
    </div>
  );
}

Gotcha

The main issue we encountered during the application development was a mutation problem. An object's reference wasn't updating so therefore the value related to it wasn't updating neither.

To solve this problem, we had to create a shallow copy of this object. Here is an example in our code.

const clearLastTile = (row: BoardGrid[0]) => {
  // shallow copy of the row to be able to mutate the tile
  const newRow = row.slice();

  // mutate reference of the tile
  for (const tile of row.reverse()) {
    if (tile.letter) {
      tile.letter = '';
      break;
    }
  }

  return newRow;
};

To conclude

As you can see SolidJS is quite similar to React in a lot of aspects, I didn't talk about memoization or context but what you can learn is that if you really need performance in your application, SolidJS might be a good choice, but in that case you might use Svelte instead. It really depends on you, but I will keep an eye on that library to see where it goes in the future.

On the other hand, Turborepo and Vanilla-extract are both excellent tools that we will surely use in our future projects at Mirahi.

Check out the codebase on our Github repository if you want to see more about the project!