Skip to content

Introduction to Vanilla Extract for CSS

Vanilla Extract is a CSS framework that lets you create class styles with TypeScript. It combines the utility class approach of something like Tailwind with the type-safety of TypeScript, allowing you to create your own custom yet consistent styles. Styles generated by Vanilla Extract are locally scoped, and compile to a single stylesheet at build time.

This introduction will show you what it looks like to use Vanilla Extract in a React app, but it's a framework-agnostic library. Anywhere you can include a class name, it should work just fine. We'll begin with very simple styles, and work our way through some of the more complex features until you've got a foundational understanding of how to utilize Vanilla Extract in your own projects.

Styles

To start with Vanilla Extract, you first create a style. The style method accepts either a style object, or an array of style objects. Here is a simple example using React. Here, we create a style, then use it in our component by passing the variable name to className. When the app builds, a stylesheet will be generated, and our exampleStyle will get a hashed class name which will be passed into the component.

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

export const exampleStyle = style({
  backgroundColor: 'blue',
  color: 'white',
  padding: '1.5rem'
});
// ExampleComponent.tsx
import React from 'react';
import { exampleStyle } from './style.css'

export const ExampleComponent = () => {
  return (
    <div className={exampleStyle}>
      This component will have a blue background, white text color, and padding of 1.5 rem.
    </div>
  );
}

When this app is built, exampleStyle gets compiled to a CSS class in a static stylesheet. That class name is then given to the React component.

<div class="style_exampleStyle__1d9bkuq0">
This component will have a blue background, white text color, and padding of 1.5 rem.
</div>
/* main.css */

.style_exampleStyle__1d9bkuq0 {
  background-color: blue;
  color: white;
  padding: 1.5rem;
}

Themes

If you want to make sure you're using consistent values across your styles, you can create a theme to make those styles available throughout your app. Using themes lets you use known names for things like standardized spacing or color palette names, allowing you to define them once upfront, and get type safety when using them later.

// theme.css.ts
import { createTheme, style } from '@vanilla-extract/css';

export const [themeClass, vars] = createTheme({
  color: {
    background: "white",
    text: "black",
    primary: "blue",
    secondary: "red",
  },
  spacing: {
    small: "4px",
    medium: "8px",
    large: "16px",
    xlarge: "32px"
  }
})
// style.css.ts
import { style } from '@vanilla-extract/css';
import { vars } from './theme.css'

export const bodyStyle = style({
  backgroundColor: vars.color.background,
  color: vars.color.text,
});

export const buttonStyle = style({
  backgroundColor: vars.color.primary,
  color: vars.color.background,
  padding: vars.sizing.large
})

Because we're using TypeScript, you can get type safety and intellisense auto-completion. If you make a typo when writing out a theme variable, your editor will warn you about it.

Variants

Sometimes you have styles that are nearly, but not quite the same. In these cases, it's sometimes best to define variants. In this example, I've created styles for two button variants: one for the primary brand colors, and one for secondary colors.

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

const colors = {
  background: "#fefefe",
  text: "#333",
  primary: "#9C19E0",
  secondary: "#32C1CD"
}

// Here is the style shared by all buttons
const baseButtonStyle = style({
  border: "none",
  borderRadius: "4px",
  fontSize: "18px"
})

// Here, we define variants of button. In this case, each key is an array containing all styles relevant to that variant. We're only combining the base style and the unique color style object.
export const buttonStyle = styleVariants({
  primary: [baseButtonStyle, {
    backgroundColor: colors.primary,
    color: colors.background
  }],
  secondary: [baseButtonStyle, {
    backgroundColor: colors.secondary,
    color: colors.text
  }]
})
// ExampleComponent.tsx
import React from 'react';
import { buttonStyle } from './style.css'

export const ExampleComponent = () => {
  return (
    <>
      <button className={buttonStyle["primary"]}>
        Primary Button
      </button>
      <button className={buttonStyle.secondary}>
        Secondary Button
      </button>
    </>
  );
}

Sprinkles

Sprinkles allow you to create easy-to-reuse utility classes with Vanilla Extract. Sprinkles allow you to define conditions, and the properties that apply under each condition, and generate all the utility classes that would be necessary to satisfy all of those potential conditions. In this example, we use defineProperties to outline some conditions and acceptable property values around colors and spacing. Then, we combine these using createSprinkles to give us a single way to use them.

// sprinkles.css.ts
import { defineProperties, createSprinkles } from "@vanilla-extract/sprinkles";

// You could also use a theme here!
const colors = {
  red: "#FF5151",
  blue: "#88E0EF",
  green: "#17D7A0"
};

const colorProperties = defineProperties({
  conditions: {
    lightMode: {
      "@media": "(prefers-color-scheme: light)"
    },
    darkMode: { "@media": "(prefers-color-scheme: dark)" }
  },
  defaultCondition: false,
  properties: {
    color: colors,
    backgroundColor: colors
  }
});

const space = {
  small: "4px",
  medium: "8px",
  large: "16px",
  xlarge: "32px"
};

const spaceProperties = defineProperties({
  conditions: {
    mobile: {},
    desktop: { "@media": "screen and (min-width: 1024px)" }
  },
  defaultCondition: "mobile",
  properties: {
    margin: space,
    padding: space
  }
});

export const sprinkles = createSprinkles(colorProperties, spaceProperties);
import React from "react";
import { sprinkles } from "./sprinkles.css";

export const ExampleComponent2 = () => {
  return (
    <div
      className={sprinkles({
        backgroundColor: {
          lightMode: "blue",
          darkMode: "red"
        },
        padding: {
          mobile: "small",
          desktop: "xlarge"
        }
      })}
    >
      This component uses sprinkles to display constrained styles based on conditions. The background color will be either #88E0EF for light mode users or #FF5151 for dark mode. And padding will default to 4px, but jump to 32px for users with a viewport min-width of 1024px.
    </div>
  );
};

Once built, the various sprinkles permutations are compiled as CSS utility classes in our stylesheet, and applied to the relevant elements like this:

<div class="
sprinkles_backgroundColor_blue_lightMode__1qjyzon8
sprinkles_backgroundColor_red_darkMode__1qjyzon7
sprinkles_padding_small_mobile__1qjyzonk
sprinkles_padding_xlarge_desktop__1qjyzonr
">
  This component uses sprinkles to display constrained styles based on conditions.
</div>

You can see, in the style names, how our conditions and properties have combined to create unique utility classes for each possibility, such as padding_small_mobile and padding_xlarge_desktop. Like with any utility class approach, be careful not to define more conditions and properties than you will actually use— you could create a very large stylesheet of mostly unused CSS that way.

If you want to combine Sprinkles utility classes with some styles unique to the element you're working on, you can combine them in an array passed to style.

export const combineStyle = style([
  sprinkles({
    // ...your sprinkles here...
  }), {
    // ...your custom styles here...
  }
])

Recipes

Recipes give you an easy way to combine base styles, multiple variants, and combinations of variants. In this example, let's revisit making different types of buttons.

import { recipe } from '@vanilla-extract/recipes';

export const button = recipe({
  // First, we define a base set of styles that apply by default.
  base: {
    border: 0,
    borderRadius: 4,
    color: "white"
  },
  // Next, we define the different variants we may want to mix-and-match.
  variants: {
    color: {
      primary: { backgroundColor: "darkslateblue" },
      secondary: { backgroundColor: "teal" },
      destructive: { backgroundColor: "tomato" }
    },
    size: {
      small: { fontSize: 10, padding: 10 },
      medium: { fontSize: 16, padding: 14 },
      large: { fontSize: 20, padding: 18 }
    }
  },
  // Optionally, we can define styles that only apply when we have specific combinations of variants. Here, we apply a different color to the text, increase font weight, and add a border for large destrucive buttons.
  compoundVariants: [
    {
      variants: {
        color: "destructive",
        size: "large"
      },
      style: {
        borderColor: "darkred",
        borderStyle: "solid",
        borderWidth: 2,
        color: "darkred",
        fontWeight: 700
      }
    }
  ],
  // Finally, we define default variants in case we don't specify any.
  defaultVariants: {
    color: "primary",
    size: "medium"
  }
});
import React from "react";
import { button } from "./style.css";

export const ExampleComponent3 = () => {
  return (
    <>
      <button className={button({
        color: "secondary",
        size: "small"
      })}>
        Secondary Small Button
      </button>
      <button className={button({
        color: "destructive",
        size: "large"
      })}>
        Destructive Large Button with Special Styles due to compoundVariants.
      </button>
    </>
  );
};

Once again, our recipe and defined variants get transformed into class names. All of the combinations necessary to get the different variants and compount variants to work have been created in the static CSS file.

<button class="style_button__1d9bkuq4 style_button_color_secondary__1d9bkuq6 style_button_size_small__1d9bkuq8">
  Secondary Small Button
</button>
<button class="style_button__1d9bkuq4 style_button_color_destructive__1d9bkuq7 style_button_size_large__1d9bkuqa style_button_compound_0__1d9bkuqb">
  Destructive Large Button
</button>
/* main.css */
.style_button__1d9bkuq4 {
  border: 0;
  border-radius: 4px;
  color: white;
}
.style_button_color_primary__1d9bkuq5 {
  background-color: darkslateblue;
}
.style_button_color_secondary__1d9bkuq6 {
  background-color: teal;
}
.style_button_color_destructive__1d9bkuq7 {
  background-color: tomato;
}
.style_button_size_small__1d9bkuq8 {
  font-size: 10px;
  padding: 10px;
}
.style_button_size_medium__1d9bkuq9 {
  font-size: 16px;
  padding: 14px;
}
.style_button_size_large__1d9bkuqa {
  font-size: 20px;
  padding: 18px;
}
.style_button_compound_0__1d9bkuqb {
  border-color: darkred;
  border-style: solid;
  border-width: 2px;
  color: darkred;
  font-weight: 700;
}

This is a basic introduction to Vanilla Extract. It's a very powerful library for creating styles, and you'll find much more information in the documentation. You can also play with all of my examples on CodeSandbox.

This Dot Labs is a development consultancy that is trusted by top industry companies, including Stripe, Xero, Wikimedia, Docusign, and Twilio. This Dot takes a hands-on approach by providing tailored development strategies to help you approach your most pressing challenges with clarity and confidence. Whether it's bridging the gap between business and technology or modernizing legacy systems, you’ll find a breadth of experience and knowledge you need. Check out how This Dot Labs can empower your tech journey.

You might also like

Testing a Fastify app with the NodeJS test runner cover image

Testing a Fastify app with the NodeJS test runner

Introduction Node.js has shipped a built-in test runner for a couple of major versions. Since its release I haven’t heard much about it so I decided to try it out on a simple Fastify API server application that I was working on. It turns out, it’s pretty good! It’s also really nice to start testing a node application without dealing with the hassle of installing some additional dependencies and managing more configurations. Since it’s got my stamp of approval, why not write a post about it? In this post, we will hit the highlights of the testing API and write some basic but real-life tests for an API server. This server will be built with Fastify, a plugin-centric API framework. They have some good documentation on testing that should make this pretty easy. We’ll also add a SQL driver for the plugin we will test. Setup Let's set up our simple API server by creating a new project, adding our dependencies, and creating some files. Ensure you’re running node v20 or greater (Test runner is a stable API as of the 20 major releases) Overview `index.js` - node entry that initializes our Fastify app and listens for incoming http requests on port 3001 `app.js` - this file exports a function that creates and returns our Fastify application instance `sql-plugin.js` - a Fastify plugin that sets up and connects to a SQL driver and makes it available on our app instance Application Code A simple first test For our first test we will just test our servers index route. If you recall from the app.js` code above, our index route returns a 501 response for “not implemented”. In this test, we're using the createApp` function to create a new instance of our Fastify app, and then using the `inject` method from the Fastify API to make a request to the `/` route. We import our test utilities directly from the node. Notice we can pass async functions to our test to use async/await. Node’s assert API has been around for a long time, this is what we are using to make our test assertions. To run this test, we can use the following command: By default the Node.js test runner uses the TAP reporter. You can configure it using other reporters or even create your own custom reporters for it to use. Testing our SQL plugin Next, let's take a look at how to test our Fastify Postgres plugin. This one is a bit more involved and gives us an opportunity to use more of the test runner features. In this example, we are using a feature called Subtests. This simply means when nested tests inside of a top-level test. In our top-level test call, we get a test parameter t` that we call methods on in our nested test structure. In this example, we use `t.beforeEach` to create a new Fastify app instance for each test, and call the `test` method to register our nested tests. Along with `beforeEach` the other methods you might expect are also available: `afterEach`, `before`, `after`. Since we don’t want to connect to our Postgres database in our tests, we are using the available Mocking API to mock out the client. This was the API that I was most excited to see included in the Node Test Runner. After the basics, you almost always need to mock some functions, methods, or libraries in your tests. After trying this feature, it works easily and as expected, I was confident that I could get pretty far testing with the new Node.js core API’s. Since my plugin only uses the end method of the Postgres driver, it’s the only method I provide a mock function for. Our second test confirms that it gets called when our Fastify server is shutting down. Additional features A lot of other features that are common in other popular testing frameworks are also available. Test styles and methods Along with our basic test` based tests we used for our Fastify plugins - `test` also includes `skip`, `todo`, and `only` methods. They are for what you would expect based on the names, skipping or only running certain tests, and work-in-progress tests. If you prefer, you also have the option of using the describe` → `it` test syntax. They both come with the same methods as `test` and I think it really comes down to a matter of personal preference. Test coverage This might be the deal breaker for some since this feature is still experimental. As popular as test coverage reporting is, I expect this API to be finalized and become stable in an upcoming version. Since this isn’t something that’s being shipped for the end user though, I say go for it. What’s the worst that could happen really? Other CLI flags —watch` - https://nodejs.org/dist/latest-v20.x/docs/api/cli.html#--watch —test-name-pattern` - https://nodejs.org/dist/latest-v20.x/docs/api/cli.html#--test-name-pattern TypeScript support You can use a loader like you would for a regular node application to execute TypeScript files. Some popular examples are tsx` and `ts-node`. In practice, I found that this currently doesn’t work well since the test runner only looks for JS file types. After digging in I found that they added support to locate your test files via a glob string but it won’t be available until the next major version release. Conclusion The built-in test runner is a lot more comprehensive than I expected it to be. I was able to easily write some real-world tests for my application. If you don’t mind some of the features like coverage reporting being experimental, you can get pretty far without installing any additional dependencies. The biggest deal breaker on many projects at this point, in my opinion, is the lack of straightforward TypeScript support. This is the test command that I ended up with in my application: I’ll be honest, I stole this from a GitHub issue thread and I don’t know exactly how it works (but it does). If TypeScript is a requirement, maybe stick with Jest or Vitest for now 🙂...

Understanding Vue's Reactive Data cover image

Understanding Vue's Reactive Data

Introduction Web development has always been about creating dynamic experiences. One of the biggest challenges developers face is managing how data changes over time and reflecting these changes in the UI promptly and accurately. This is where Vue.js, one of the most popular JavaScript frameworks, excels with its powerful reactive data system. In this article, we dig into the heart of Vue's reactivity system. We unravel how it perfectly syncs your application UI with the underlying data state, allowing for a seamless user experience. Whether new to Vue or looking to deepen your understanding, this guide will provide a clear and concise overview of Vue's reactivity, empowering you to build more efficient and responsive Vue 3 applications. So, let’s kick off and embark on this journey to decode Vue's reactive data system. What is Vue's Reactive Data? What does it mean for data to be ”'reactive”? In essence, when data is reactive, it means that every time the data changes, all parts of the UI that rely on this data automatically update to reflect these changes. This ensures that the user is always looking at the most current state of the application. At its core, Vue's Reactive Data is like a superpower for your application data. Think of it like a mirror - whatever changes you make in your data, the user interface (UI) reflects these changes instantly, like a mirror reflecting your image. This automatic update feature is what we refer to as “reactivity”. To visualize this concept, let's use an example of a simple Vue application displaying a message on the screen: `javascript import { createApp, reactive } from 'vue'; const app = createApp({ setup() { const state = reactive({ message: 'Hello Vue!' }); return { state }; } }); app.mount('#app'); ` In this application, 'message' is a piece of data that says 'Hello Vue!'. Let's say you change this message to 'Goodbye Vue!' later in your code, like when a button is clicked. `javascript state.message = 'Goodbye Vue!'; ` With Vue's reactivity, when you change your data, the UI automatically updates to 'Goodbye Vue!' instead of 'Hello Vue!'. You don't have to write extra code to make this update happen - Vue's Reactive Data system takes care of it. How does it work? Let's keep the mirror example going. Vue's Reactive Data is the mirror that reflects your data changes in the UI. But how does this mirror know when and what to reflect? That's where Vue's underlying mechanism comes into play. Vue has a behind-the-scenes mechanism that helps it stay alerted to any changes in your data. When you create a reactive data object, Vue doesn't just leave it as it is. Instead, it sends this data object through a transformation process and wraps it up in a Proxy. Proxy objects are powerful and can detect when a property is changed, updated, or deleted. Let's use our previous example: `javascript import { createApp, reactive } from 'vue'; const app = createApp({ setup() { const state = reactive({ message: 'Hello Vue!' }); return { state }; } }); app.mount('#app'); ` Consider our “message” data as a book in a library. Vue places this book (our data) within a special book cover (the Proxy). This book cover is unique - it's embedded with a tracking device that notifies Vue every time someone reads the book (accesses the data) or annotates a page (changes the data). In our example, the reactive function creates a Proxy object that wraps around our state object. When you change the 'message': `javascript state.message = 'Goodbye Vue!'; ` The Proxy notices this (like a built-in alarm going off) and alerts Vue that something has changed. Vue then updates the UI to reflect this change. Let’s look deeper into what Vue is doing for us and how it transforms our object into a Proxy object. You don't have to worry about creating or managing the Proxy; Vue handles everything. `javascript const state = reactive({ message: 'Hello Vue!' }); // What vue is doing behind the scenes: function reactive(obj) { return new Proxy(obj, { // target = state and key = message get(target, key) { track(target, key) return target[key] }, set(target, key, value) { target[key] = value // Here Vue will trigger its reactivity system to update the DOM. trigger(target, key) } }) } ` In the example above, we encapsulate our object, in this case, “state”, converting it into a Proxy object. Note that within the second argument of the Proxy, we have two methods: a getter and a setter. The getter method is straightforward: it merely returns the value, which in this instance is “state.message” equating to 'Hello Vue!' Meanwhile, the setter method comes into play when a new value is assigned, as in the case of “state.message = ‘Hey young padawan!’”. Here, “value” becomes our new 'Hey young padawan!', prompting the property to update. This action, in turn, triggers the reactivity system, which subsequently updates the DOM. Venturing Further into the Depths If you have been paying attention to our examples above, you might have noticed that inside the Proxy` method, we call the functions `track` and `trigger` to run our reactivity. Let’s try to understand a bit more about them. You see, Vue 3 reactivity data is more about Proxy objects. Let’s create a new example: `vue import { reactive, watch, computed, effect } from "vue"; const state = reactive({ showSword: false, message: "Hey young padawn!", }); function changeMessage() { state.message = "It's dangerous to go alone! Take this."; } effect(() => { if (state.message === "It's dangerous to go alone! Take this.") { state.showSword = true; } }); {{ state.message }} Click! ` In this example, when you click on the button, the message's value changes. This change triggers the effect function to run, as it's actively listening for any changes in its dependencies__. How does the effect` property know when to be called? Vue 3 has three main functions to run our reactivity: effect`, `track`, and `trigger`. The effect` function is like our supervisor. It steps in and takes action when our data changes – similar to our effect method, we will dive in more later. Next, we have the track` function. It notes down all the important data we need to keep an eye on. In our case, this data would be `state.message`. Lastly, we've got the trigger` function. This one is like our alarm bell. It alerts the `effect` function whenever our important data (the stuff `track` is keeping an eye on) changes. In this way, trigger`, `track`, and `effect` work together to keep our Vue application reacting smoothly to changes in data. Let’s go back to them: `javascript function reactive(obj) { return new Proxy(obj, { get(target, key) { // target = state & key = message track(target, key) // keep an eye for this return target[key] }, set(target, key, value) { target[key] = value trigger(target, key) // trigger the effects! } }) } ` Tracking (Dependency Collection) Tracking is the process of registering dependencies between reactive objects and the effects that depend on them. When a reactive property is read, it's "tracked" as a dependency of the current running effect. When we execute track()`, we essentially store our effects in a Set object. But what exactly is an "effect"? If we revisit our previous example, we see that the effect method must be run whenever any property changes. This action — running the effect method in response to property changes — is what we refer to as an "Effect"! (computed property, watcher, etc.) > Note: We'll outline a basic, high-level overview of what might happen under the hood. Please note that the actual implementation is more complex and optimized, but this should give you an idea of how it works. Let’s see how it works! In our example, we have the following reactive object: `javascript const state = reactive({ showSword: false, message: "Hey young padawn!", }); // which is transformed under the hood to: function reactive(obj) { return new Proxy(obj, { get(target, key) { // target = state | key = message track(target, key) // keep an eye for this return target[key] }, set(target, key, value) { target[key] = value trigger(target, key) // trigger the effects! } }) } ` We need a way to reference the reactive object with its effects. For that, we use a WeakMap. Which type is going to look something like this: `typescript WeakMap>> ` We are using a WeakMap to set our object state as the target (or key). In the Vue code, they call this object `targetMap`. Within this targetMap` object, our value is an object named `depMap` of Map type. Here, the keys represent our properties (in our case, that would be `message` and `showSword`), and the values correspond to their effects – remember, they are stored in a Set that in Vue 3 we refer to as `dep`. Huh… It might seem a bit complex, right? Let's make it more straightforward with a visual example: With the above explained, let’s see what this Track` method kind of looks like and how it uses this `targetMap`. This method essentially is doing something like this: `javascript let activeEffect; // we will see more of this later function track(target, key) { if (activeEffect) { // depsMap` maps targets to their keys and dependent effects let depsMap = targetMap.get(target); // If we don't have a depsMap for this target in our targetMap`, create one. if (!depsMap) { depsMap = new Map(); targetMap.set(target, depsMap); } let dep = depsMap.get(key); if (!dep) { // If we don't have a set of effects for this key in our depsMap`, create one. dep = new Set(); depsMap.set(key, dep); } // Add the current effect as a dependency dep.add(activeEffect); } } ` At this point, you have to be wondering, how does Vue 3 know what activeEffect` should run? Vue 3 keeps track of the currently running effect by using a global variable. When an effect is executed, Vue temporarily stores a reference to it in this global variable, allowing the track` function to access the currently running effect and associate it with the accessed reactive property. This global variable is called inside Vue as `activeEffect`. Vue 3 knows which effect is assigned to this global variable by wrapping the effects functions in a method that invokes the effect whenever a dependency changes. And yes, you guessed, that method is our effect` method. `javascript effect(() => { if (state.message === "It's dangerous to go alone! Take this.") { state.showSword = true; } }); ` This method behind the scenes is doing something similar to this: `javascript function effect(update) { //the function we are passing in const effectMethod = () => { // Assign the effect as our activeEffect` activeEffect = effectMethod // Runs the actual method, also triggering the get` trap inside our proxy update(); // Clean the activeEffect after our Effect has finished activeEffect = null } effectMethod() } ` The handling of activeEffect` within Vue's reactivity system is a dance of careful timing, scoping, and context preservation. Let’s go step by step on how this is working all together. When we run our `Effect` method for the first time, we call the `get` trap of the Proxy. `javascript function effect(update) const effectMethod = () => { // Storing our active effect activeEffect = effectMethod // Running the effect update() ... } ... } effect(() => { // we call the the get` trap when getting our `state.message` if (state.message === "It's dangerous to go alone! Take this.") { state.showSword = true; } }); ` When running the get` trap, we have our `activeEffect` so we can store it as a dependency. `javascript function reactive(obj) { return new Proxy(obj, { // Gets called when our effect runs get(target, key) { track(target, key) // Saves the effect return target[key] }, // ... (other handlers) }) } function track(target, key) { if (activeEffect) { //... rest of the code // Add the current effect as a dependency dep.add(activeEffect); } } ` This coordination ensures that when a reactive property is accessed within an effect, the track function knows which effect is responsible for that access. Trigger Method Our last method makes this Reactive system to be complete. The trigger` method looks up the dependencies for the given target and key and re-runs all dependent effects. `javascript function trigger(target, key) { const depsMap = targetMap.get(target); if (!depsMap) return; // no dependencies, no effects, no need to do anything const dep = depsMap.get(key); if (!dep) return; // no dependencies for this key, no need to do anything // all dependent effects to be re-run dep.forEach(effect => { effect() }); } ` Conclusion Diving into Vue 3's reactivity system has been like unlocking a hidden superpower in my web development toolkit, and honestly, I've had a blast learning about it. From the rudimentary elements of reactive data and instantaneous UI updates to the intricate details involving Proxies, track and trigger functions, and effects, Vue 3's reactivity is an impressively robust framework for building dynamic and responsive applications. In our journey through Vue 3's reactivity, we've uncovered how this framework ensures real-time and precise updates to the UI. We've delved into the use of Proxies to intercept and monitor variable changes and dissected the roles of track and trigger functions, along with the 'effect' method, in facilitating seamless UI updates. Along the way, we've also discovered how Vue ingeniously manages data dependencies through sophisticated data structures like WeakMaps and Sets, offering us a glimpse into its efficient approach to change detection and UI rendering. Whether you're just starting with Vue 3 or an experienced developer looking to level up, understanding this reactivity system is a game-changer. It doesn't just streamline the development process; it enables you to create more interactive, scalable, and maintainable applications. I love Vue 3, and mastering its reactivity system has been enlightening and fun. Thanks for reading, and as always, happy coding!...

Linting, Formatting, and Type Checking Commits in an Nx Monorepo with Husky and lint-staged cover image

Linting, Formatting, and Type Checking Commits in an Nx Monorepo with Husky and lint-staged

One way to keep your codebase clean is to enforce linting, formatting, and type checking on every commit. This is made very easy with pre-commit hooks. Using Husky, you can run arbitrary commands before a commit is made. This can be combined with lint-staged, which allows you to run commands on only the files that have been staged for commit. This is useful because you don't want to run linting, formatting, and type checking on every file in your project, but only on the ones that have been changed. But if you're using an Nx monorepo for your project, things can get a little more complicated. Rather than have you use eslint or prettier directly, Nx has its own scripts for linting and formatting. And type checking is complicated by the use of specific tsconfig.json files for each app or library. Setting up pre-commit hooks with Nx isn't as straightforward as in a simpler repository. This guide will show you how to set up pre-commit hooks to run linting, formatting, and type checking in an Nx monorepo. Configure Formatting Nx comes with a command, nx format:write for applying formatting to affected files which we can give directly to lint-staged. This command uses Prettier under the hood, so it will abide by whatever rules you have in your root-level .prettierrc file. Just install Prettier, and add your preferred configuration. ` npm install --save-dev prettier ` Then add a .prettierrc file to the root of your project with your preferred configuration. For example, if you want to use single quotes and trailing commas, you can add the following: ` { "singleQuote": true, "trailingComma": "all" } ` Configure Linting Nx has its own plugin that uses ESLint to lint projects in your monorepo. It also has a plugin with sensible ESLint defaults for your linter commands to use, including ones specific to Nx. To install them, run the following command: ` npm i --save-dev @nrwl/linter @nrwl/eslint-plugin-nx ` Then, we can create a default .eslintrc.json file in the root of our project: ` { "root": true, "ignorePatterns": ["/*"], "plugins": ["@nrwl/nx"], "overrides": [ { "files": [".ts", "*.tsx", "*.js", "*.jsx"], "rules": { "@nrwl/nx/enforce-module-boundaries": [ "error", { "enforceBuildableLibDependency": true, "allow": [], "depConstraints": [ { "sourceTag": "", "onlyDependOnLibsWithTags": [""] } ] } ] } }, { "files": [".ts", "*.tsx"], "extends": ["plugin:@nrwl/nx/typescript"], "rules": {} }, { "files": [".js", "*.jsx"], "extends": ["plugin:@nrwl/nx/javascript"], "rules": {} } ] } ` The above ESLint configuration will, by default, apply Nx's module boundary rules to any TypeScript or JavaScript files in your project. It also applies its recommended rules for JavaScript and TypeScript respectively, and gives you room to add your own. You can also have ESLint configurations specific to your apps and libraries. For example, if you have a React app, you can add a .eslintrc.json file to the root of your app directory with the following contents: ` { "extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"], "rules": { "no-console": ["error", { "allow": ["warn", "error"] }] } } ` Set Up Type Checking Type checking with tsc is normally a very straightforward process. You can just run tsc --noEmit to check your code for type errors. But things are more complicated in Nx with lint-staged. There are a two tricky things about type checking with lint-staged in an Nx monorepo. First, different apps and libraries can have their own tsconfig.json files. When type checking each app or library, we need to make sure we're using that specific configuration. The second wrinkle comes from the fact that lint-staged passes a list of staged files to commands it runs by default. And tsc will only accept either a specific tsconfig file, or a list of files to check. We do want to use the specific tsconfig.json files, and we also only want to run type checking against apps and libraries with changes. To do this, we're going to create some Nx run commands within our apps and libraries and run those instead of calling tsc directly. Within each app or library you want type checked, open the project.json file, and add a new run command like this one: ` { // ... "targets": { // ... "typecheck": { "executor": "nx:run-commands", "options": { "commands": ["tsc -p tsconfig.app.json --noEmit"], "cwd": "apps/directory-of-your-app-goes-here", "forwardAllArgs": false } }, } } ` Inside commands is our type-checking command, using the local tsconfig.json file for that specific Nx app. The cwd option tells Nx where to run the command from. The forwardAllArgs option tells Nx to ignore any arguments passed to the command. This is important because tsc will fail if you pass both a tsconfig.json and a list of files from lint-staged. Now if we ran nx affected --target=typecheck from the command line, we would be able to type check all affected apps and libraries that have a typecheck target in their project.json. Next we'll have lint-staged handle this for us. Installing Husky and lint-staged Finally, we'll install and configure Husky and lint-staged. These are the two packages that will allow us to run commands on staged files before a commit is made. ` npm install --save-dev husky lint-staged ` In your package.json file, add the prepare script to run Husky's install command: ` { "scripts": { "prepare": "husky install" } } ` Then, run your prepare script to set up git hooks in your repository. This will create a .husky directory in your project root with the necessary file system permissions. ` npm run prepare ` The next step is to create our pre-commit hook. We can do this from the command line: ` npx husky add .husky/pre-commit "npx lint-staged --concurrent false --relative" ` It's important to use Husky's CLI to create our hooks, because it handles file system permissions for us. Creating files manually could cause problems when we actually want to use the git hooks. After running the command, we will now have a file at .husky/pre-commit that looks like this: ` !/usr/bin/env sh . "$(dirname -- "$0")//husky.sh" npx lint-staged --concurrent false --relative ` Now whenever we try to commit, Husky will run the lint-staged command. We've given it some extra options. First, --concurrent false to make sure attempts to write fixes with formatting and linting don't conflict with simultaneous attempts at type checking. Second is --relative, because our Nx commands for formatting and linting expect a list of file paths relative to the repo root, but lint-staged would otherwise pass the full path by default. We've got our pre-commit command ready, but we haven't actually configured lint-staged yet. Let's do that next. Configuring lint-staged In a simpler repository, it would be easy to add some lint-staged configuration to our package.json file. But because we're trying to check a complex monorepo in Nx, we need to add a separate configuration file. We'll call it lint-staged.config.js and put it in the root of our project. Here is what our configuration file will look like: ` module.exports = { '{apps,libs,tools}//*.{ts,tsx}': files => { return nx affected --target=typecheck --files=${files.join(',')}`; }, '{apps,libs,tools}//*.{js,ts,jsx,tsx,json}': [ files => nx affected:lint --files=${files.join(',')}`, files => nx format:write --files=${files.join(',')}`, ], }; ` Within our module.exports object, we've defined two globs: one that will match any TypeScript files in our apps, libraries, and tools directories, and another that also matches JavaScript and JSON files in those directories. We only need to run type checking for the TypeScript files, which is why that one is broken out and narrowed down to only those files. These globs defining our directories can be passed a single command, or an array of commands. It's common with lint-staged to just pass a string like tsc --noEmit or eslint --fix. But we're going to pass a function instead to combine the list of files provided by lint-staged with the desired Nx commands. The nx affected and nx format:write commands both accept a --files option. And remember that lint-staged always passes in a list of staged files. That array of file paths becomes the argument to our functions, and we concatenate our list of files from lint-staged into a comma-delimitted string and interpolate that into the desired Nx command's --files option. This will override Nx's normal behavior to explicitly tell it to only run the commands on the files that have changed and any other files affected by those changes. Testing It Out Now that we've got everything set up, let's try it out. Make a change to a TypeScript file in one of your apps or libraries. Then try to commit that change. You should see the following in your terminal as lint-staged runs: ` Preparing lint-staged... Running tasks for staged files... lint-staged.config.js {apps,libs,tools}//*.{ts,tsx} nx affected --target=typecheck --files=apps/your-app/file-you-changed.ts {apps,libs,tools}//*.{js,ts,jsx,tsx,json} nx affected:lint --files=apps/your-app/file-you-changed.ts nx format:write --files=apps/your-app/file-you-changed.ts Applying modifications from tasks... Cleaning up your temporary files... ` Now, whenever you try to commit changes to files that match the globs defined in lint-staged.config.js, the defined commands will run first, and verify that the files contain no type errors, linting errors, or formatting errors. If any of those commands fail, the commit will be aborted, and you'll have to fix the errors before you can commit. Conclusion We've now set up a monorepo with Nx and configured it to run type checking, linting, and formatting on staged files before a commit is made. This will help us catch errors before they make it into our codebase, and it will also help us keep our codebase consistent and readable. To see an example Nx monorepo with these configurations, check out this repo....

Nuxt DevTools v1.0: Redefining the Developer Experience Beyond Conventional Tools cover image

Nuxt DevTools v1.0: Redefining the Developer Experience Beyond Conventional Tools

In the ever-evolving world of web development, Nuxt.js has taken a monumental leap with the launch of Nuxt DevTools v1.0. More than just a set of tools, it's a game-changer—a faithful companion for developers. This groundbreaking release, available for all Nuxt projects and being defaulted from Nuxt v3.8 onwards, marks the beginning of a new era in developer tools. It's designed to simplify our development journey, offering unparalleled transparency, performance, and ease of use. Join me as we explore how Nuxt DevTools v1.0 is set to revolutionize our workflow, making development faster and more efficient than ever. What makes Nuxt DevTools so unique? Alright, let's start delving into the features that make this tool so amazing and unique. There are a lot, so buckle up! In-App DevTools The first thing that caught my attention is that breaking away from traditional browser extensions, Nuxt DevTools v1.0 is seamlessly integrated within your Nuxt app. This ensures universal compatibility across browsers and devices, offering a more stable and consistent development experience. This setup also means the tools are readily available in the app, making your work more efficient. It's a smart move from the usual browser extensions, making it a notable highlight. To use it you just need to press Shift + Option + D` (macOS) or `Shift + Alt + D` (Windows): With simple keystrokes, the Nuxt DevTools v1.0 springs to life directly within your app, ready for action. This integration eliminates the need to toggle between windows or panels, keeping your workflow streamlined and focused. The tools are not only easily accessible but also intelligently designed to enhance your productivity. Pages, Components, and Componsables View The Pages, Components, and Composables View in Nuxt DevTools v1.0 are a clear roadmap for your app. They help you understand how your app is built by simply showing its structure. It's like having a map that makes sense of your app's layout, making the complex parts of your code easier to understand. This is really helpful for new developers learning about the app and experienced developers working on big projects. Pages View lists all your app's pages, making it easier to move around and see how your site is structured. What's impressive is the live update capability. As you explore the DevTools, you can see the changes happening in real-time, giving you instant feedback on your app's behavior. Components View is like a detailed map of all the parts (components) your app uses, showing you how they connect and depend on each other. This helps you keep everything organized, especially in big projects. You can inspect components, change layouts, see their references, and filter them. By showcasing all the auto-imported composables, Nuxt DevTools provides a clear overview of the composables in use, including their source files. This feature brings much-needed clarity to managing composables within large projects. You can also see short descriptions and documentation links in some of them. Together, these features give you a clear picture of your app's layout and workings, simplifying navigation and management. Modules and Static Assets Management This aspect of the DevTools revolutionizes module management. It displays all registered modules, documentation, and repository links, making it easy to discover and install new modules from the community! This makes managing and expanding your app's capabilities more straightforward than ever. On the other hand, handling static assets like images and videos becomes a breeze. The tool allows you to preview and integrate these assets effortlessly within the DevTools environment. These features significantly enhance the ease and efficiency of managing your app's dynamic and static elements. The Runtime Config and Payload Editor The Runtime Config and Payload Editor in Nuxt DevTools make working with your app's settings and data straightforward. The Runtime Config lets you play with different configuration settings in real time, like adjusting settings on the fly and seeing the effects immediately. This is great for fine-tuning your app without guesswork. The Payload Editor is all about managing the data your app handles, especially data passed from server to client. It's like having a direct view and control over the data your app uses and displays. This tool is handy for seeing how changes in data impact your app, making it easier to understand and debug data-related issues. Open Graph Preview The Open Graph Preview in Nuxt DevTools is a feature I find incredibly handy and a real time-saver. It lets you see how your app will appear when shared on social media platforms. This tool is crucial for SEO and social media presence, as it previews the Open Graph tags (like images and descriptions) used when your app is shared. No more deploying first to check if everything looks right – you can now tweak and get instant feedback within the DevTools. This feature not only streamlines the process of optimizing for social media but also ensures your app makes the best possible first impression online. Timeline The Timeline feature in Nuxt DevTools is another standout tool. It lets you track when and how each part of your app (like composables) is called. This is different from typical performance tools because it focuses on the high-level aspects of your app, like navigation events and composable calls, giving you a more practical view of your app's operation. It's particularly useful for understanding the sequence and impact of events and actions in your app, making it easier to spot issues and optimize performance. This timeline view brings a new level of clarity to monitoring your app's behavior in real-time. Production Build Analyzer The Production Build Analyzer feature in Nuxt DevTools v1.0 is like a health check for your app. It looks at your app's final build and shows you how to make it better and faster. Think of it as a doctor for your app, pointing out areas that need improvement and helping you optimize performance. API Playground The API Playground in Nuxt DevTools v1.0 is like a sandbox where you can play and experiment with your app's APIs. It's a space where you can easily test and try out different things without affecting your main app. This makes it a great tool for trying out new ideas or checking how changes might work. Some other cool features Another amazing aspect of Nuxt DevTools is the embedded full-featured VS Code. It's like having your favorite code editor inside the DevTools, with all its powerful features and extensions. It's incredibly convenient for making quick edits or tweaks to your code. Then there's the Component Inspector. Think of it as your code's detective tool. It lets you easily pinpoint and understand which parts of your code are behind specific elements on your page. This makes identifying and editing components a breeze. And remember customization! Nuxt DevTools lets you tweak its UI to suit your style. This means you can set up the tools just how you like them, making your development environment more comfortable and tailored to your preferences. Conclusion In summary, Nuxt DevTools v1.0 marks a revolutionary step in web development, offering a comprehensive suite of features that elevate the entire development process. Features like live updates, easy navigation, and a user-friendly interface enrich the development experience. Each tool within Nuxt DevTools v1.0 is thoughtfully designed to simplify and enhance how developers build and manage their applications. In essence, Nuxt DevTools v1.0 is more than just a toolkit; it's a transformative companion for developers seeking to build high-quality web applications more efficiently and effectively. It represents the future of web development tools, setting new standards in developer experience and productivity....