Skip to content

Strong Typing the State and Actions in NgRx

Strong Typing the State and Actions

When working with NgRx store, it is highly recommended to provide strong and explicit types for both the State and Actions. This becomes even more significant as our application will inevitably grow, which means it will need more features and almost certainly some refactoring along the way. This is where strong types might make this process easier and safe.

I'll base this article on a simple Angular app where we can display a list of photos that then can be liked or disliked. You can find the source code of this application on my GitHub repo. If you want to follow this article's code, please clone the repository and checkout strongTypingState_entryPoint tag.

git clone git@github.com:ktrz/introduction-to-ngrx.git git checkout strongTypingState_entryPoint

After cloning, install all the dependencies: yarn install

You can see the example app by running: yarn start -o

Typing Actions

With the latest version of NgRx, typing actions is very straight forward. Quoting the docs:

The createAction function returns a function, that when called returns an object in the shape of the Action interface. The props method is used to define any additional metadata needed for the handling of the action. Action creators provide a consistent, type-safe way to construct an action that is being dispatched.

Creating actions for liking and disliking a photo could look like this:

// src/app/store/photo.actions.ts

import {createAction, props} from '@ngrx/store';

export const likePhoto = createAction(
  '[Photo List] Like Photo',
  props<{id: string}>()
);

export const dislikePhoto = createAction(
  '[Photo List] Dislike Photo',
  props<{id: string}>()
);

This creates concise and type-safe actions and action creators all-in-one. It also doesn't produce as much boilerplate as the class approach, which was used in previous versions of NgRx (and can still be found in many production code repositories):

// src/app/store/photo.actions.ts

import {Action} from '@ngrx/store';

const enum PhotoActionTypes {
  LikePhoto = '[Photo List] Like Photo',
  DislikePhoto = '[Photo List] Dislike Photo'
}

class LikePhoto implements Action {
  readonly type = PhotoActionTypes.LikePhoto;

  constructor(public readonly id: string) {}
}

class DislikePhoto implements Action {
  readonly type = PhotoActionTypes.DislikePhoto;

  constructor(public readonly id: string) {}
}

export type PhotoActions = LikePhoto | DislikePhoto;

In this example, we need to create classes for each action that act as action creators. However, it is good practice to extract all possible action types into an enum or a set of consts and a separate type union PhotoActions to use later ie. in reducers. All this behavior is neatly packed into the createAction utility function so for creating new actions, I highly suggest using it.

Typing State

When it comes to typing state, it's a good practice to type every slice of the state containing a specific feature separately. A good place to include it is a reducer file which will handle this specific slice of the state. For larger projects, you can also keep your state types in a separate file ie. src/app/store/photo.state.ts

// src/app/store/photo.state.ts

export interface Photo {
  id: string;
  title: string;
  url: string;
  likes: number;
  dislikes: number;
}

export interface PhotoState {
  [id: string]: Photo;
}

export const photoFeatureKey = 'photo';

export interface PhotoRootState {
  [photoFeatureKey]: PhotoState;
}

Typing rest of NgRx chain (implicitly)

By having both State and Actions strongly typed, all created reducers, selectors, and effects can easily infer further types and keep the rest of our NgRx chain type-safe.

import {createReducer, on} from '@ngrx/store';
import {dislikePhoto, likePhoto} from './photo.actions';
import {PhotoState} from './photo.state';

const initialState: PhotoState = {};

export const photoReducer = createReducer(
  initialState,
  on(likePhoto, (state, action) => ({
    ...state,
    [action.id]: {
      ...state[action.id],
      likes: state[action.id].likes + 1
    }
  })),
  on(dislikePhoto, (state, action) => ({
    ...state,
    [action.id]: {
      ...state[action.id],
      dislikes: state[action.id].dislikes + 1
    }
  }))
);

By providing initialState to createReducer utility function, our photoReducer is strongly typed to operate only on PhotoState type.

Each on(...) call uses a TypeScript type inference from the provided action (likePhoto, dislikePhoto) so that

on(likePhoto, (state, action) => {/* ... /*})

is actually strongly typed as

// this is a bit simplified type than the actual inferred type
// for a sake keeping it easier to grasp
type LikeActionType = {id: string, type: '[Photo List] Like Photo'}

on(likePhoto, (state: PhotoState, action: LikeActionType): PhotoState => {/* ... /*})

The same rules apply to building selectors from our state

// src/app/store/photo.selectors.ts

import {createFeatureSelector, createSelector} from '@ngrx/store';
import {photoFeatureKey, PhotoRootState, PhotoState} from './photo.reducer';

const selectPhotoFeature = createFeatureSelector<PhotoRootState, PhotoState>(photoFeatureKey);

export const selectPhotos = createSelector(selectPhotoFeature, state => Object.keys(state).map(key => state[key]));

export const selectPhoto = createSelector(selectPhotoFeature, (state: PhotoState, props: {id: string}) => state[props.id]);

By providing strong explicit typings for selectPhotoFeature, TypeScript will usually be able to infer types for all the other selectors derived from it. When creating a new derived selector:

export const selectPhotos = createSelector(selectPhotoFeature, state => Object.keys(state).map(key => state[key]));

it is equivalent to explicitly typing everything like so

export const selectPhotos = createSelector<PhotoRootState, PhotoState, Photo[]>(selectPhotoFeature, (state: PhotoState): Photo[] => Object.keys(state).map(key => state[key]));

Not everything use case can be inferred automatically but usually, a small hint for a TS compiler is enough

export const selectPhoto = createSelector(selectPhotoFeature, (state, props: {id: string}) => state[props.id]);

state param can't be automatically inferred and has any type by default. Angular will complain about it in a strict mode, so in order to complete the typing, we can explicitly add the proper PhotoState type here.

export const selectPhoto = createSelector(selectPhotoFeature, (state: PhotoState, props: {id: string}) => state[props.id]);

Benefits

In conclusion, by providing strong typing for just Actions and State, we get typings in other parts of NgRx chain usually for free (or by providing minimal hints for the TS compiler). This means that we can benefit both from our IDE auto completion when writing the code. It also provides us with a safety net in case of doing some refactoring or adding new functionality to the state. For example, if we modify the shape of the state in order to accommodate for a new feature, we will immediately be notified by the TS compiler or our IDE of which other parts of the app chain are affected. This way we can review all of them more easily. When combining that with a high test coverage, we can have a good level of confidence to modify the code without breaking anything in the process.

You can find the code for this article's end result on my GitHub repo. Checkout strongTypingState_ready tag to get the up-to-date and ready-to-run solution.

If you have any questions, you can always tweet or DM me @ktrz. I'm always happy to help!

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: ` Compared to the NgModules way of adding components, as shown below, you can immediately see how standalone components make things much simpler. ` 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. ` 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. ``` 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. ` 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. ` 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: ` 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: ` For adding SSR to an already existing project, utilize the ng add command of the Angular CLI: ` 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: ` Then, you need to add it to the provideRouter configuration: ` 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: ` 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. ` 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: ` 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': ` 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. ` 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: ` 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: ` 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: ` 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: ` 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: ` 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. ` This method behind the scenes is doing something similar to this: ` 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. ` When running the get trap, we have our activeEffect so we can store it as a dependency. ` 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. ` 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!...

Introduction to Directives Composition API in Angular cover image

Introduction to Directives Composition API in Angular

In version 15, Angular introduced a new directives composition API that allows developers to compose existing directives into new more complex directives or components. This allows us to encapsulate behaviors into smaller directives and reuse them across the application more easily. In this article, we will explore the new API, and see how we can use it in our own components. All the examples from this article (and more) can be found on Stackblitz here. Starting point In the article, I will use two simple directives as an example * HighlightDirective - a directive borrowed from Angular's Getting started guide. This directive will change an element's background color whenever the element hovers. ` Fig. 1 * BorderDirective - a similar directive that will apply a border of a specified color to the element whenever it hovers ` Fig. 2 We can now easily apply our directives to any element we want ie. a paragraph: ` Fig. 3 However, if we wanted to apply both highlighting and border on hover we would need to add both directives explicitly: ` Fig. 4 With the new directives composition API, we can easily create another directive that composes behaviors of our 2 directives. Host Directives Angular 15 added a new property to the @Directive and @Component decorators. In this property, we can specify an array of different directives that we want our new component or directive to apply on a host element. We can do it as follows: ` As you can see in the above example, by just defining the hostDirectives property containing our highlight and border directives, we created a new directive that composes both behaviors into one directive. We can now achieve the same result as in Fig. 4 by using just a single directive: ` Fig. 5 Passing inputs and outputs Our newly composed directive works nicely already, but there is a problem. How do we pass properties to the directives that are defined in the hostDirectives array? They are not passed by default, but we can configure them to do so pretty easily by using an extended syntax: ` Fig. 6 This syntax takes exposes the "inner" directives\ color input from the HighlightAndBorderDirective`, and passes them down to both highlight and border directives. ` Fig. 7 This works, but we ended up with a border and highlight color both being blue. Luckily Angular's API allows us to easily redefine the properties' names using the : syntax. So let's remap out properties to highlightColor and borderColor so that the names don't collide with each other: ` Fig. 8 Now we can control both colors individually: ` Fig. 9 We could apply the same approach to mapping directive's outputs eg. ` Fig. 10 or ` Fig. 11 Adding host directives to a component Similarly to composing a directive out of other directives, we can apply the same approach to adding behavior to components using hostDirectives API. This way, we could, for example, create a more specialized component or just apply the behavior of the directive to a whole host element: ` Fig. 12 This component will render the paragraph, and apply both directives' behavior to the host element: ` Fig. 13 Just like we did for the directive, we can also expose and remap the directives inputs using the extended syntax. But if we would like to access and modify the directives inputs from within our component, we can also do that. This is where Angular's dependency injection comes in handy. We can inject the host directives via a constructor just like we would do for a service. After we have the directives instances available, we can modify them ie. in the ngOnInit lifecycle hook: ` Fig. 14 With this change, the code from Fig. 13 will use lightcoral as a background color and red as a border color. Performance Note While this API gives us a powerful tool-set for reusing behaviors across different components, it can impact the performance of our application if used excessively. For each instance of a given composed component Angular will create objects of the component class itself as well as an instance of each directive that it is composed of. If the component appears only a couple of times in the application. then it won't make a significant difference. However, if we create, for example, a composed checkbox component that appears hundreds of times in the app, this may have a noticeable performance impact. Please make sure you use this pattern with caution, and profile your application in order to find the right composition pattern for your application. Summary As I have shown in the above examples, the directives composition API can be a quite useful but easy-to-use tool for extracting behaviors into smaller directives and combining them into more complex behaviors. In case you have any questions, you can always tweet or DM me at @ktrz. I'm always happy to help!...

Building interactive forms with TanStack Form cover image

Building interactive forms with TanStack Form

TanStack Form is a new headless form library that makes building more complex and interactive forms easy. TanStack has understood the power of ‘headless’ UI libraries for quite some time with some really incredible tools like TanStack Table and TanStack Query. Given the high quality of all the other libraries in the TanStack suite, I was excited to give this new form library a try. If you’ve used react-hook-form before the two libraries have quite a bit in common. The transition should be mostly straightforward. Let's start by comparing the two libraries and bit and then dig into the TanStack Form API with some examples and code. Comparison to react-hook-form At a high level, the two libraries are pretty similar: - Headless, hook-based API - Performance (both libraries minimize amount of renders triggered by state changes) - Lightweight, zero-dependencies (TanStack 4.4kb, react-hook-form 9.7kb) - Comprehensive feature set - Type-safety / TypeScript support There are a few things that set TanStack Form apart: - UI Agnostic - TanStack Form offers support for other UI libraries and frameworks through a plugin-style system. Currently, React is supported out of the box. - Simpler API and documentation - The API surface area is a bit smaller, and the documentation makes it easy to find the details on all the APIs. - First-class TypeScript support - TanStack Form provides really impressive type inference. It stays out of your way and lets you focus on writing code instead of wrangling types. One common feature not supported in TanStack Form currently is validation schemas. react-hook-form supports integration with validation libraries like Zod and Yup. You can plug in a schema and react-hook-form will automatically validate your form against the schema. Building forms Since the API for TanStack Form is pretty small, we can walk through the important APIs pretty quickly to see how they work together to help us build interactive client-side forms. useForm(options) useForm accepts an options object that accepts defaultValues, defaultState, and event handler functions like onSubmit, onChange, etc. The types are clear, and the source code is easy to read so you can refer to FormApi.ts file for more specifics. Currently, the examples on the website don’t provide a type for the generic TData that useForm accepts. It will infer the form types based on the defaultValues that are provided, but in this example, I will provide the type explicitly. ` The API for your form is returned from the useForm hook. We will use this API to build the fields and their functionality in our component. useField(opts) If you want to abstract one of your form fields into its own component we can use the useField hook. The useField generic parameters take the type definition of your form and the field name. ` Validation is as simple as returning a string from your field onChange handler. For our password field we return an error message if the length isn’t at least 8 characters long. We bind our field states and event handlers to the form input by passing them in as props. Component API form includes a component API with a Context Provider, a component for rendering Field components, and a Subscribe component for subscribing to state changes. I’ve added the remaining fields to our form using the Field component and included our PasswordField from above. Using the Field component is similar to using the useField hook - the difference is our field instance gets passed in via a render prop. ` We wrapped our submit button with the Subscribe component which has a pretty interesting API. Subscribe allows you to subscribe to state changes granularly. Whatever state items you return from the selector gets passed into the render prop and re-render it on changes. This will be really useful in places where render performance is critical. Async event handlers We can use the built-in async handlers and debounce options to implement a search input. This feature makes a common pattern like this trivial. ` Async event handlers can be used for other things like async validation as well. We could update our email input from our create account form to check to see if the account already exists. ` Conclusion If you don’t mind taking a chance on a newer library from a well-established author in the ecosystem, Tanstack Form is a solid choice. The APIs are easy to understand, and it has some nice async event handling features that are extremely useful. If validation with deep integration with a schema library like Zod is a requirement, you might want to stick with react-hook-form for now. You can definitely still use a validation library with TanStack Form, it will just take a little more work on your part to get it all wired up....