Skip to content

How to create reusable form components with React Hook Forms and Typescript

Why Should I Create Reusable react-hook-form Input Components?

Like many in the React community, you've decided to use react-hook-form. While not every situation calls for creating wrapper components around react-hook-form, there are some situations where doing exactly that may be ideal. One such example might be creating a reusable component library for your organization. You might want to reuse the same validation logic and error styling in multiple projects. Maybe your project contains many large forms and in this case, reusable react-hook-form components can save you a lot of time. If you've decided reusable react-hook-form components are what's right for you and your team, it can be hard to understand how to make these components especially if you and/or your team have also decided to use Typescript.

Step 1: Create An Input Component

The first step is to create an input component. Creating an isolated component can be a good way to provide consumers of your component with a way to use inputs that aren't directly tied to validation or react-hook-form. It can also help consolidate styles and isolate input logic for easier unit testing. You can also use this component to enforce good accessibility practices by ensuring that inputs have a label, type, and name.

Step 2: Creating a Form

Before we move forward, we need a form. Let's imagine a registration form.

import React, { FC } from 'react';
import { Input } from '../atoms/input';

export const RegistrationForm: FC = () => {
  return (
    <form>
      <Input
        id="firstName"
        type="text"
        name="firstName"
        label="First Name"
        placeholder="First Name"
      />
    </form>
  );
};

Eventually, we'll want to make the first name field in this form a required field and display an error if that field is not provided, but before we can add any validation and see errors, we'll need to add a way to submit the form.

In our <RegistrationForm>, let's destructure the handleSubmit function off of our call to useForm, then use that to create an onSubmit function we'll call for our form submission:

import React, { FC } from 'react';
import { useForm } from 'react-hook-form';
import { Input } from '../atoms/input';

export type RegistrationFormFields = {
  firstName: string;
};

export const RegistrationForm: FC = () => {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<RegistrationFormFields>();

  const onSubmit = handleSubmit((data) => {
    console.log('submitting...');
  });

  return (
    <form onSubmit={onSubmit}>
      <Input
        id="firstName"
        type="text"
        name="firstName"
        label="First Name"
        placeholder="First Name"
      />
      <button
        className="mt-4 transform duration-200 py-2 px-4 bg-blue-500 text-white font-semibold rounded shadow-md hover:bg-blue-600 focus:outline-none disabled:opacity-50 focus:translate-y-1 hover:-translate-y-1"
        type="submit"
      >
        Submit
      </button>
    </form>
  );
};

We should have something that looks like this:

Step 3: Create a Validated Input Component

We want create a wrapper component that uses both our <Input> component and react-hook-form to create a reusable component that can be passed any validation rules and any potential errors. Let's make a new component called <FormInput>.

import React from 'react';
import { Input, InputProps } from '../atoms/input';

export type FormInputProps = InputProps;

export const FormInput = ({
  className,
  ...props
}: FormInputProps): JSX.Element => {
  return (
    <div className={className} aria-live="polite">
      <Input {...props} />
    </div>
  );
};

Registering Our Input with react-hook-form

Next, we need to register our input with react-hook-form. Errors and rules won't work until we do this. It may seem like our first step is to pass a register property given to us by react-hook-form to our generic <FormInput> component. When we attempt to do this, we'll encounter an issue. When defining our register property, what type should we assign to register?

import React from 'react';
import { InputProps } from '../atoms/input';

export type FormInputProps = {
  register: ???;
} & InputProps;

Let's take a look at what react-hook-form says is the type of register: Typescript Type for Register

We can see that the type of register is UserFormRegister<RegistrationFormFields>, but passing in a register property of type UserFormRegister<RegistrationFormFields> wouldn't help us keep our <FormInput> component generic. Not every form will have inputs that belong to our registration form. Each form will likely have different validation rules and errors. Now what?

The answer lies in generics. We need our <FormInput> to have a register property that can take in any type of form. Let's change the type for our <FormInput>'s properties.

import React from 'react';
import { InputProps } from '../atoms/input';

export type FormInputProps<TFormValues> = {
  register: ???;
} & InputProps;

<TFormValues> is a generic type that represents any potential form. In this case, our form type is RegistrationFormFields, but using <TFormValues> means that this component can be used in any form. Next, let's take a look at how we can pass the register property to our input.

import React from 'react';
import { UseFormRegister } from 'react-hook-form';
import { InputProps } from '../atoms/input';

export type FormInputProps<TFormValues> = {
  register?: UseFormRegister<TFormValues>;
};

Now, we can register an input used in a form of any type with react-hook-form. We aren't quite done with register yet, however. We still need to tell react-hook-form which input/field in the form we are trying to register. We'll need to do this with a name property that we will pass into the register function.

Defining the Name Property

It may seem like our new name property should be a string, because it represents the name of one of the fields in our form. Upon close inspection, we can see that the type of parameter register is expecting is a Path to one of the fields in our TFormValues.

The reason name is a Path is because our form type, TFormValues could potentially have nested fields, like objects, or arrays. For example, imagine that for some reason, instead of registering just one person, we had a list of people to register. Our RegistrationFormFields might look something like this instead:

export type RegistrationFormFields = {
  people: { firstName: string }[];
};

If the name property of our input field was just a string, we couldn't reference the name of the first or second person in our form. When name is of type Path, however we can access the fields of any nested object or array. For example, name could be people[0].firstName or people.[1].firstName.

The properties for our <FormInput> component should now look like:

import React from 'react';
import { UseFormRegister, Path } from 'react-hook-form';
import { InputProps } from '../atoms/input';

export type FormInputProps<TFormValues> = {
  name: Path<TFormValues>;
  register?: UseFormRegister<TFormValues>;
} & InputProps;

But wait! Now both InputProps and our FormInputProps have a name. These names are not the same thing either! The name in InputProps represents the native name on an HTML input element. The name in FormInputProps is a Path to the field in our form. We still want to be able to pass other native HTML attributes to our input. We'd just like to exclude name from InputProps because we are defining name as a Path FormInputProps`.

import React from 'react';
import { UseFormRegister, Path } from 'react-hook-form';
import { InputProps } from '../atoms/input';

export type FormInputProps<TFormValues> = {
  name: Path<TFormValues>;
  register?: UseFormRegister<TFormValues>;
} & Omit<InputProps, 'name'>;

The last thing we need to register our input with our react-hook form is validation rules. We will need to pass our validation rules to this <FormInput>, and then into the register function.

Defining the Rules Property

Remember, we want to be able to pass in any rules to our <FormInput> component, not just rules that apply to our registration form. Thankfully, this one is a bit easier.

import React from 'react';
import { UseFormRegister, Path, RegisterOptions } from 'react-hook-form';
import { InputProps } from '../atoms/input';

export type FormInputProps<TFormValues> = {
  name: Path<TFormValues>;
  rules?: RegisterOptions;
  register?: UseFormRegister<TFormValues>;
} & Omit<InputProps, 'name'>;

Let's use these properties to actually register, apply rules, and render our <FormInput> component. We also want to replace the <Input> component we were using with our new <FormInput> component in our <RegistrationForm>. We should have something that looks like this:

Sweet! We're still missing something though. No errors are actually displayed when we don't enter a first name. Let's fix that.

Defining the Errors Property

When defining our errors property, what type should we assign to errors? Let's take a look at what react-hook-form says the type of errors is. Typescript Type for Errors

We can see that the type of errors is:

{
  firstName?: FieldError;
}

Like before, passing in an errors property of type { firstName?: FieldError; } wouldn't help us keep our <FormInput> component generic. Not every form will have errors for a registration form. Each form will likely have errors. We'll have to use our <TFormValues> generic again:

import React from 'react';
import {
  RegisterOptions,
  DeepMap,
  FieldError,
  UseFormRegister,
  Path,
} from 'react-hook-form';
import { InputProps } from '../atoms/input';

export type FormInputProps<TFormValues> = {
  name: Path<TFormValues>;
  rules?: RegisterOptions;
  register?: UseFormRegister<TFormValues>;
  errors?: DeepMap<TFormValues, FieldError>;
} & Omit<InputProps, 'name'>;

You might be wondering how we could conclude that the type of errors should be DeepMap<TFormValues, FieldError>. This one was a bit tricky, but eventually we can find the type by looking for FieldErrors in the TS documentation for react-hook-form.

Let's pass the errors we're getting from react-hook-form in our <RegistrationForm> into the errors property of the <FormInput>. Errors Typescript Type Error

Looks like the errors type we are passing in from useForm does not match the errors type we are expecting in our <FormInput>.

The errors type we are passing in from useForm has a type that looks like this: UseForm Errors Type UseForm Errors Type

The errors property we are expecting in our <FormInput> looks like this:

...
export type FormInputProps<TFormValues> = {
  ...
  errors?: DeepMap<TFormValues, FieldError>;
} & Omit<InputProps, 'name'>;

It looks like our errors property on our <FormInput> is of type DeepMap<TFormValues, FieldError>. It requires that either errors are completely empty, errors?, or that every property in our TFormValues exists on our errors. This isn't quite accurate.

There won't always be an error for every single field in our form. Our errors will dynamically change as the user inputs data. There might be an error for the firstName field, but if the firstName field is valid, there won't be an error for it. So although our RegistrationFormFields says that firstName is not optional, our errors must declare that any firstName errors are optional. We can achieve this by making our errors property on our <FormInput> a <Partial>.

...
export type FormInputProps<TFormValues> = {
  ...
  errors?: Partial<DeepMap<TFormValues, FieldError>>;
} & Omit<InputProps, 'name'>;

This means that the errors don't have to include all of the fields in our TFormValues, and with that our errors Typescript error is now gone!

Fixed Errors Typescript Type Error

Displaying Errors

To make sure that our error styling is consolidated, and the same across our inputs, we'll want to put the code to display these errors in our <FormInput> component.

Remember, errors contains all the errors for our form, not just the errors for one field. To render our errors, we'll want the get the errors relevant to this specific input. Your first instinct might be to get the errors by the name of our input, like so:

export const FormInput = <TFormValues extends Record<string, unknown>>({
  name,
  register,
  rules,
  errors,
  className,
  ...props
}: FormInputProps<TFormValues>): JSX.Element => {
  const errorMessages = errors[name];
  const hasError = !!(errors && errorMessages);

  return (
    <div className={className} aria-live="polite">
      <Input name={name} {...props} {...(register && register(name, rules))} />
      {hasError && <p>{errorMessages}</p>}
    </div>
  );
};

However, name is a Path. It could reference a field on a nested object or in an array. For example, we'd need to get errors.people[0].firstName and not errors['people[0].firstName'].

To do this, we can make use of lodash.get to retrieve the value of errors via a Path;

export const FormInput = <TFormValues extends Record<string, unknown>>({
  name,
  register,
  rules,
  errors,
  className,
  ...props
}: FormInputProps<TFormValues>): JSX.Element => {
  // If the name is in a FieldArray, it will be 'fields.index.fieldName' and errors[name] won't return anything, so we are using lodash get
  const errorMessages = get(errors, name);
  const hasError = !!(errors && errorMessages);

  return (
    <div className={className} aria-live="polite">
      <Input name={name} {...props} {...(register && register(name, rules))} />
      {hasError && <p>{errorMessages}</p>}
    </div>
  );
};

We're still missing something, though. Currently, errorMessages is an object representing each violated rule for our input.

For example, let's imagine that we had an email field in addition to our firstName field. This email field should not only be required, but should have a minimum of 4 chars entered, and match a valid email format.

If more than one rule was violated, like in the case that the email entered was both too short and didn't match the email format, our errorMessages would have multiple keys called pattern and minLength. Pattern would indicate that the email format was not valid. minLength would indicate that the email entered was not long enough.

However, most of the time, we only want to display only error message at a time. But how do we know which error message to display in an generic input component? Every form's type will be structured completely differently. We don't know what the shape of errors will be. How can we display specific error messages when the type could look like anything?

Fortunately, there's another package we can use that contains a component that can do this for us. That component is called ErrorMessage, and it is provided by @hookform/error-message. Using yarn or npm, install this package.

npm install @hookform/error-message or yarn add @hookform/error-message

Once that package is added, we can use it in our <FormInput>:

import React from 'react';
import classNames from 'classnames';
import get from 'lodash.get';

import {
  RegisterOptions,
  DeepMap,
  FieldError,
  UseFormRegister,
  Path,
} from 'react-hook-form';
import { ErrorMessage } from '@hookform/error-message';
import { Input, InputProps } from '../atoms/input';

export type FormInputProps<TFormValues> = {
  name: Path<TFormValues>;
  rules?: RegisterOptions;
  register?: UseFormRegister<TFormValues>;
  errors?: Partial<DeepMap<TFormValues, FieldError>>;
} & Omit<InputProps, 'name'>;

export const FormInput = <TFormValues extends Record<string, unknown>>({
  name,
  register,
  rules,
  errors,
  className,
  ...props
}: FormInputProps<TFormValues>): JSX.Element => {
  // If the name is in a FieldArray, it will be 'fields.index.fieldName' and errors[name] won't return anything, so we are using lodash get
  const errorMessages = get(errors, name);
  const hasError = !!(errors && errorMessages);

  return (
    <div className={className} aria-live="polite">
      <Input
        name={name}
        aria-invalid={hasError}
        className={classNames({
          'transition-colors focus:outline-none focus:ring-2 focus:ring-opacity-50 border-red-600 hover:border-red-600 focus:border-red-600 focus:ring-red-600':
            hasError,
        })}
        {...props}
        {...(register && register(name, rules))}
      />
      <ErrorMessage
        errors={errors}
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        name={name as any}
        render={({ message }) => (
          <p className="mt-1 font-serif text-sm text-left block text-red-600">
            {message}
          </p>
        )}
      />
    </div>
  );
};

The <ErrorMessage> component takes in an errors property. This is the same as the errors we passed into our <FormInput>, and represents all errors for all fields in the form.

The second property we are passing into <ErrorMessage> is name. The type of name on <ErrorMessage> does not match the name property we are taking into <FormInput>. For some reason, the name is of type FieldName<FieldValuesFromFieldErrors<DeepMap<TFormValues, FieldError>>> on the <ErrorMessage> component, but is of type Path<TFormValues> in the register call, even though both name properties represent are both the Path of a field in the form. Because the FieldValuesFromFieldErrors type isn't exported, I couldn't cast name to FieldName<FieldValuesFromFieldErrors<DeepMap<TFormValues, FieldError>>> and therefore had to cast it to any.

The last property we are passing into <ErrorMessage> is a render function. This render function gives you access to the single error message you'd like to display. You can use this function to display the message in any way you'd like.

Conclusion

You now know a bit more about extracting validation logic from react-hook-forms to create reusable components in Typescript. You can continue to use this pattern to create more form components like checkboxes, phone number inputs, select boxes and more. In my next blog, I'll cover how to use a different schema validation resolver, like yup, and how to display server side validation errors using the same components. Take a look at the demo below to see a closer to real life example.

Live Demo

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

Angular 17: Continuing the Renaissance cover image

Angular 17: Continuing the Renaissance

Angular 17: A New Era November 8th marked a significant milestone in the world of Angular with the release of Angular 17. This wasn't just any ordinary update; it was a leap forward, signifying a new chapter for the popular framework. But what made this release truly stand out was the unveiling of Angular's revamped website, complete with a fresh brand identity and a new logo. This significant transformation represents the evolving nature of Angular, aligning with the modern demands of web development. To commemorate this launch, we also hosted a release afterparty, where we went deep into its new features with Minko Gechev from the Angular core team, and Google Developer Experts (GDEs) Brandon Roberts, Deborah Kurata, and Enea Jahollari. But what exactly are these notable new features in the latest version? Let's dive in and explore. The Angular Renaissance Angular has been undergoing a significant revival, often referred to as Angular's renaissance, a term coined by Sarah Drasner, the Director of Engineering at Google, earlier this year. This revival has been particularly evident in its recent versions. The Angular team has worked hard to introduce many new improvements, focusing on signal-based reactivity, hydration, server-side rendering, standalone components, and migrating to esbuild and Vite for a better and faster developer experience. This latest release, in particular, marks many of these features as production-ready. Standalone Components About a year ago, Angular began a journey toward modernity with the introduction of standalone components. This move significantly enhanced the developer experience, making Angular more contemporary and user-friendly. In Angular's context, a standalone component is a self-sufficient, reusable code unit that combines logic, data, and user interface elements. What sets these components apart is their independence from Angular's NgModule system, meaning they do not rely on it for configuration or dependencies. By setting a standalone: true` flag, you no longer need to embed your component in an NgModule and you can bootstrap directly off that component: `typescript // ./app/app.component.ts @Component({ selector: 'app', template: 'hello', standalone: true }) export class AppComponent {} // ./main.ts import { bootstrapApplication } from '@angular/platform-browser'; import { AppComponent } from './app/app.component'; bootstrapApplication(AppComponent).catch(e => console.error(e)); ` Compared to the NgModules way of adding components, as shown below, you can immediately see how standalone components make things much simpler. `ts // ./app/app.component.ts import { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'], }) export class AppComponent { title = 'CodeSandbox'; } // ./app/app.module.ts import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } // .main.ts import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.error(err)); ` In this latest release, the Angular CLI now defaults to generating standalone components, directives, and pipes. This default setting underscores the shift towards a standalone-centric development approach in Angular. New Syntax for Enhanced Control Flow Angular 17 introduces a new syntax for control flow, replacing traditional structural directives like ngIf` or `ngFor`, which have been part of Angular since version 2. This new syntax is designed for fine-grained change detection and eventual zone-less operation when Angular completely migrates to signals. It's more streamlined and performance-efficient, making handling conditional or list content in templates easier. The @if` block replaces `*ngIf` for expressing conditional parts of the UI. `ts @if (a > b) { {{a}} is greater than {{b}} } @else if (b > a) { {{a}} is less than {{b}} } @else { {{a}} is equal to {{b}} } ` The @switch` block replaces `ngSwitch`, offering benefits such as not requiring a container element to hold the condition expression or each conditional template. It also supports template type-checking, including type narrowing within each branch. ```ts @switch (condition) { @case (caseA) { Case A. } @case (caseB) { Case B. } @default { Default case. } } ``` The @for` block replaces `*ngFor` for iteration and presents several differences compared to its structural directive predecessor, `ngFor`. For example, the tracking expression (calculating keys corresponding to object identities) is mandatory but offers better ergonomics. Additionally, it supports `@empty` blocks. `ts @for (item of items; track item.id) { {{ item.name }} } ` Defer Block for Lazy Loading Angular 17 introduces the @defer` block, a dramatically improving lazy loading of content within Angular applications. Within the `@defer` block framework, several sub-blocks are designed to elegantly manage different phases of the deferred loading process. The main content within the `@defer` block is the segment designated for lazy loading. Initially, this content is not rendered, becoming visible only when specific triggers are activated or conditions are met, and after the required dependencies have been loaded. By default, the trigger for a `@defer` block is the browser reaching an idle state. For instance, take the following block: it delays the loading of the calendar-imp` component until it comes into the viewport. Until that happens, a placeholder is shown. This placeholder displays a loading message when the `calendar-imp` component begins to load, and an error message if, for some reason, the component fails to load. `ts @defer (on viewport) { } @placeholder { Calendar placeholder } @loading { Loading calendar } @error { Error loading calendar } ` The on` keyword supports a wide a variety of other conditions, such as: - idle` (when the browser has reached an idle state) - interaction` (when the user interacts with a specified element) - hover` (when the mouse has hovered over a trigger area) - timer(x)` (triggers after a specified duration) - immediate` (triggers the deferred load immediately) The second option of configuring when deferring happens is by using the when` keyword. For example: `ts @defer (when isVisible) { } ` Server-Side Rendering (SSR) Angular 17 has made server-side rendering (SSR) much more straightforward. Now, a --ssr` option is included in the `ng new` command, removing the need for additional setup or configurations. When creating a new project with the `ng new` command, the CLI inquires if SSR should be enabled. As of version 17, the default response is set to 'No'. However, for version 18 and beyond, the plan is to enable SSR by default in newly generated applications. If you prefer to start with SSR right away, you can do so by initializing your project with the `--ssr` flag: `shell ng new --ssr ` For adding SSR to an already existing project, utilize the ng add` command of the Angular CLI: `shell ng add @angular/ssr ` Hydration In Angular 17, the process of hydration, which is essential for reviving a server-side rendered application on the client-side, has reached a stable, production-ready status. Hydration involves reusing the DOM structures rendered on the server, preserving the application's state, and transferring data retrieved from the server, among other crucial tasks. This functionality is automatically activated when server-side rendering (SSR) is used. It offers a more efficient approach than the previous method, where the server-rendered tree was completely replaced, often causing visible UI flickers. Such re-rendering can adversely affect Core Web Vitals, including Largest Contentful Paint (LCP), leading to layout shifts. By enabling hydration, Angular 17 allows for the reuse of the existing DOM, effectively preventing these flickers. Support for View Transitions The new View Transitions API, supported by some browsers, is now integrated into the Angular router. This feature, which must be activated using the withViewTransitions` function, allows for CSS-based animations during route transitions, adding a layer of visual appeal to applications. To use it, first you need to import withViewTransitions`: `ts import { provideRouter, withViewTransitions } from '@angular/router'; ` Then, you need to add it to the provideRouter` configuration: `ts bootstrapApplication(AppComponent, { providers: [ provideRouter(routes, withViewTransitions()) ] }) ` Other Notable Changes - Angular 17 has stabilized signals, initially introduced in Angular 16, providing a new method for state management in Angular apps. - Angular 17 no longer supports Node 16. The minimal Node version required is now 18.13. - TypeScript version 5.2 is the least supported version starting from this release of Angular. - The @Component` decorator now supports a `styleUrl` attribute. This allows for specifying a single stylesheet path as a string, simplifying the process of linking a component to a specific style sheet. Previously, even for a single stylesheet, an array was required under `styleUrls`. Conclusion With the launch of Angular 17, the Angular Renaissance is now in full swing. This release has garnered such positive feedback that developers are showing renewed interest in the framework and are looking forward to leveraging it in upcoming projects. However, it's important to note that it might take some time for IDEs to adapt to the new templating syntax fully. While this transition is underway, rest assured that you can still write perfectly valid code using the old templating syntax, as all the changes in Angular 17 are backward compatible. Looking ahead, the future of Angular appears brighter than ever, and we can't wait to see what the next release has in store!...

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

Software Team Leadership: Risk Taking & Decision Making with David Cramer, Co-Founder & CTO at Sentry cover image

Software Team Leadership: Risk Taking & Decision Making with David Cramer, Co-Founder & CTO at Sentry

In this episode of the engineering leadership series, Rob Ocel interviews David Cramer, co-founder and CTO of Sentry, delving into the importance of decision-making, risk-taking, and the challenges faced in the software engineering industry. David emphasizes the significance of having conviction and being willing to make decisions, even if they turn out to be wrong. He shares his experience of attending a CEO event, where he discovered that decision-making and conflict resolution are struggles even for successful individuals. David highlights the importance of making decisions quickly and accepting the associated risks, rather than attempting to pursue multiple options simultaneously. He believes that being decisive is crucial in the fast-paced software engineering industry. This approach allows for faster progress and adaptation, even if it means occasionally making mistakes along the way. The success of Sentry is attributed to a combination of factors, including market opportunity and the team's principles and conviction. David acknowledges that bold ideas often carry a higher risk of failure, but if they do succeed, the outcome can be incredibly significant. This mindset has contributed to Sentry’s achievements in the industry. The interview also touches on the challenges of developing and defending opinions in the software engineering field. David acknowledges that it can be difficult to navigate differing viewpoints and conflicting ideas. However, he emphasizes the importance of standing by one's convictions and being open to constructive criticism and feedback. Throughout the conversation, David emphasizes the need for engineering leaders to be decisive and take calculated risks. He encourages leaders to trust their instincts and make decisions promptly, even if they are uncertain about the outcome. This approach fosters a culture of innovation and progress within engineering teams. The episode provides valuable insights into the decision-making process and the challenges faced by engineering leaders. It highlights the importance of conviction, risk-taking, and the ability to make decisions quickly in the software engineering industry. David's experiences and perspectives offer valuable lessons for aspiring engineering leaders looking to navigate the complexities of the field....