Skip to content

A Guide to (Typed) Reactive Forms in Angular - Part II (Building Dynamic Superforms)

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

In the first blog post of the series, we learned about Angular reactive forms and the data structures behind them. When developing real-world applications, however, you often need to leverage dynamic forms, as writing boilerplate for every form and its specific cases can be tedious and time-consuming. In certain situations, it may even be necessary to retrieve information from an API to construct the forms.

In this post, we will go over a convenient abstraction we can create to build dynamic and adaptable forms without repeating boilerplate. The trick is to create a "view model" for our data and use a service to transform that data into a reactive form. I was first introduced to this approach by my friend and former teammate Thomas Duft, and I've been using it ever since.

The approach outlined in the linked article worked great with untyped forms, but since now we can get our forms strictly typed, we'll want to upgrade it.

And here is where it gets a bit tricky. Remember how I mentioned you shouldn't predeclare your form groups earlier? If you want to recursively create a form from a config, you just have to. And it's a dynamic form, so you cannot easily type it. To solve the issue, I devised a trick inspired by a "Super Form" suggested by Bobby Galli. Assuming we will have interfaces defined for our data, using this approach, we can create dynamic type-safe forms.

First, we'll create types for our form config:

// this will be our ViewModel for configuring a FormGroup
export class FormSection<
  T extends {
    [K in keyof T]:
      | FormSection<any>
      | FormField<any>
      | (FormSection<any> | FormField<any>)[];
  } = any
> {
  public title?: string;
  public fields: T;

  constructor(section: {
    title?: string;
    fields: T;
  }) {
    this.title = section.title;
    this.fields = section.fields;
  }
}

// Let's define some editor types we'll be using in the templates later
export type FormEditor =
  | 'textInput'
  | 'passwordInput'
  | 'textarea'
  | 'checkbox'
  | 'select';

// And this will be a ViewModel for our FormControls
export class FormField<T> {
  public value: T;
  public editor: FormEditor;
  public validators: Validators;
  public label: string;
  public required: boolean;
  public options?: T[];

  constructor(field: {
    value: T;
    editor: FormEditor;
    validators: Validators;
    label: string;
    required: boolean;
    options?: T[];
  }) {
    this.value = field.value;
    this.editor = field.editor;
    this.validators = field.validators;
    this.label = field.label;
    this.required = field.required;
    this.options = field.options;
  }
}

And then we'll create some type mappings:

// We will use this type mapping to properly declare our form group
export type ControlsOf<T extends Record<string, any>> = {
  [K in keyof T]: T[K] extends Array<any>
    ? FormArray<AbstractControl<T[K][0]>>
    : T[K] extends Record<any, any>
    ? FormGroup<ControlsOf<T[K]>>
    : FormControl<T[K] | null>;
};

// We will use this type mapping to type our form config
export type ConfigOf<T> = {
  [K in keyof T]: T[K] extends (infer U)[]
    ? U extends Record<any, any>
      ? FormSection<ConfigOf<U>>[]
      : FormField<U>[]
    : T[K] extends Record<any, any>
    ? FormSection<ConfigOf<T[K]>>
    : FormField<T[K]>;
};

And now we can use our types in a service that will take care of creating nested dynamic forms:

import { Injectable } from '@angular/core';
import {
  AbstractControl,
  FormArray,
  FormControl,
  FormGroup,
} from '@angular/forms';
import { ConfigOf, ControlsOf, FormField, FormSection } from './forms.model';

@Injectable({
  providedIn: 'root',
})
export class FormsService {
  public createFormGroup<T extends Record<string, any>>(
    section: FormSection<ConfigOf<T>>
  ): FormGroup<ControlsOf<T>> {
    // we need to create an empty FormGroup first, so we can add FormControls recursively
    const group = new FormGroup({});

    Object.keys(section.fields).forEach((key: any) => {
      const field = section.fields[key];
      if (Array.isArray(field)) {
        group.addControl(key, this.createFormArray(field));
      } else {
        if (field instanceof FormSection) {
          group.addControl(key, this.createFormGroup(field));
        } else {
          group.addControl(key, new FormControl(field.value, field.validators));
        }
      }
    });

    // and we need to cast the group to the correct type before returning
    return group as unknown as FormGroup<ControlsOf<T>>;
  }

  public createFormArray<T extends Record<string, any>>(
    fields: unknown[]
  ): FormArray<AbstractControl<T>> {
    const array: FormArray<AbstractControl<any>> = new FormArray(
      []
    ) as unknown as FormArray<AbstractControl<T>>;

    fields.forEach((field) => {
      if (field instanceof FormSection) {
        array.push(this.createFormGroup(field));
      } else {
        const { value, validators } = field as FormField<T>;
        array.push(new FormControl(value, validators));
      }
    });

    return array as unknown as FormArray<AbstractControl<T>>;
  }
}

And that's it. Now we can use our FormService to create forms. Let's say we have the following User model:

export type User = {
  email: string;
  name: string;
}

We can create a form for this user from config in the following way:

  const userFormConfig = new FormSection<ConfigOf<User>>({
      title: 'User Form',
      fields: {
        email: new FormField<string>({
          value: '',
          validators: [Validators.required, Validators.email],
          label: 'Email',
          editor: 'textInput',
          required: true,
        }),
        name: new FormField<string>({
          value: '',
          validators: [Validators.required],
          label: 'Name',
          editor: 'textInput',
          required: true,
        })
      }
  });

  const userForm = this.formsService.createFormGroup<User>(userFormConfig);

If we would check the type of userForm.value now, we would see that it's correctly inferred as:

Partial<{
    email: string | null;
    name: string | null;
}>

Outputting the Dynamic Forms

To display the dynamic forms, we can write a simple component that takes the FormSection or FormField as an Input() along with our FormGroup and displays the form recursively.

We can use a setter to assign either field or section property when the view model is passed into the component, so we can conveniently use them in our template. Our form component's TypeScript code will look something like this:

import { Component, Input } from '@angular/core';
import { FormField, FormSection } from '../forms.model';
import { FormArray, FormGroup } from '@angular/forms';

@Component({
  selector: 'app-form',
  templateUrl: './form.component.html',
  styleUrls: ['./form.component.scss'],
})
export class FormComponent {
  private fieldConfig?: FormField<any>;
  private sectionConfig?: FormSection<any>;
  private arrayConfig?: (FormSection<any> | FormField<any>)[];
  private sectionFieldsArray?: [string, FormField<any>][];

  @Input()
  public set config(
    config:
      | FormField<any>
      | FormSection<any>
      | (FormSection<any> | FormField<any>)[]
  ) {
    this.fieldConfig = config instanceof FormField ? config : undefined;
    this.arrayConfig = Array.isArray(config) ? config : undefined;
    this.sectionConfig = config instanceof FormSection ? config : undefined;

    this.sectionFieldsArray = Object.entries(this.sectionConfig?.fields || {});
  }

  public get sectionFields(): [string, FormField<any>][] {
    return this.sectionFieldsArray || [];
  }

  public get field(): FormField<any> | undefined {
    return this.fieldConfig;
  }

  public get section(): FormSection<any> | undefined {
    return this.sectionConfig;
  }

  public get array(): (FormSection<any> | FormField<any>)[] | undefined {
    return this.arrayConfig;
  }

  ngAfterViewInit() {
    console.log(this.arrayConfig);
  }

  @Input()
  public key!: string;

  @Input()
  public group!: FormGroup;

  public get sectionGroup(): FormGroup {
    return this.group.get(this.key) as FormGroup;
  }

  public get formArray(): FormArray {
    return this.group.get(this.key) as FormArray;
  }
}

And our template will reference a new form component for each section field in case we have passed in a FormSection and it will have a switch case to display the correct control in case a FormField has been passed in:

<ng-container *ngIf="field">
  <label>{{ field.label }}</label>
  <div [ngSwitch]="field.editor" [formGroup]="group">
    <textarea *ngSwitchCase="'textarea'" [formControlName]="key"></textarea>
    <input
      *ngSwitchCase="'passwordInput'"
      [formControlName]="key"
      type="input"
    />
    <input *ngSwitchCase="'checkbox'" [formControlName]="key" type="checkbox" />
    <input *ngSwitchDefault [formControlName]="key" type="text" />
  </div>
</ng-container>

<ng-container *ngIf="section">
  <div>
    <h3 *ngIf="section?.title">{{ section.title }}</h3>
    <app-form
      *ngFor="let sectionField of sectionFields"
      [config]="sectionField[1]"
      [key]="sectionField[0]"
      [group]="key ? sectionGroup : group"
    ></app-form>
  </div>
</ng-container>

<ng-container *ngIf="array">
  <div [formGroup]="group">
    <div [formArrayName]="key">
      <div *ngFor="let item of array; let i = index">
        <app-form
          [config]="item"
          [key]="i.toString()"
          [group]="sectionGroup"
        ></app-form>
      </div>
    </div>
  </div>
</ng-container>

That way, we can display the whole form just by referencing one component, such as

 <app-form [config]="formViewModel" [group]="form"></app-form>

Check out an example on StackBlitz.

In the next (and last) post of the series, we will learn about building custom Form Controls.

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

You might also like

Angular 18 Announced: Zoneless Change Detection and More cover image

Angular 18 Announced: Zoneless Change Detection and More

Angular 18 Announced: Zoneless Change Detection and More Angular 18 has officially landed, and yet again, the Angular team has proven that they are listening to the community and are committed to continuing the framework's renaissance. The release polishes existing features, addresses common developer requests, and introduces experimental zoneless change detection. Let's examine the major updates and enhancements that Angular 18 offers. 1. Zoneless Angular One of the most exciting features of Angular 18 is the introduction of experimental support for zoneless change detection. Historically, Zone.js has been responsible for triggering Angular's change detection. However, this approach has downsides, especially regarding performance and debugging complexity. The Angular team has been working towards making Zone.js optional, and Angular 18 marks a significant milestone in this journey. Key Features of Zoneless Angular 1. Hybrid Change Detection: In Angular 18, a new hybrid change detection mode is enabled by default. This mode allows Angular to listen to changes from signals and other notifications regarding changes that occur either inside an Angular zone or not. That effectively means you can write (library) code that works regardless of whether Zone.js is being used, paving the way to fully zoneless apps without compromising backward compatibility. For new applications, Angular enables zone coalescing by default, which removes the number of change detection cycles and improves performance. For existing applications, you can enable zone coalescing by configuring your NgZone provider in bootstrapApplication: ` 2. Experimental API for Zoneless Mode: Angular 18 introduces an experimental API to disable Zone.js entirely. This API allows developers to run applications in a fully zoneless mode, paving the way for improved performance and simpler debugging. The zoneless change detection requires an entirely new scheduler which relies on notifications from the Angular core APIs, such as ChangeDetectorRef.markForCheck (called automatically by the AsyncPipe), ComponentRef.setInput, signal updates, host listener updates, or attaching a view that was marked dirty. 3. Improved Composability and Interoperability: Zoneless change detection enhances composability for micro-frontends and interoperability with other frameworks. It also offers faster initial render and runtime, smaller bundle sizes, more readable stack traces, and simpler debugging. How to Try Zoneless Angular To experiment with zoneless change detection, you can add the provideExperimentalZonelessChangeDetection provider to your application bootstrap: ` After adding the provider, remove Zone.js from your polyfills in angular.json. You can read more about the experimental zoneless change detection in the official documentation. By the way, angular.dev is now considered the official home for Angular developers! 2. Server-side Rendering and Hydration Enhancements A feature I'm particularly excited about is the improvements to Angular's server-side rendering (SSR) and hydration in terms of developer experience and debugging: 1. Enhanced DevTools Support: Angular DevTools now includes overlays and detailed error breakdowns to help visualize and debug hydration issues directly in the browser. This refreshing focus on developer experience shows that the Angular team wants to make the framework more approachable and user-friendly. 2. Hydration Support for Angular Material: All Angular Material components now support client hydration and are no longer skipped, which enhances performance and user experience. 3. Event Replay: Available in developer preview, event replay captures user interactions during SSR and replays them once the application is hydrated, ensuring a seamless user experience before complete hydration. It is powered by the same library as Google Search. 4. i18n Hydration Support: Up to v17, Angular skipped hydration for components with i18n blocks. From v18, hydration support for i18n blocks is in developer preview, allowing developers to use client hydration in internationalized applications. 3. Stable Material Design 3 After introducing experimental support for Material Design 3 in Angular 17, Angular 18 now includes stable support. The key features of Material Design 3 in Angular 18 include: 1. Simplified Theme Styles: Based on CSS variables, the new theming styles offer more granular customization and a flexible API for applying color variants to components. 2. Theming Generation Schematics: Using the Ng CLI, you can generate Material 3 themes. 3. Sass APIs: New Sass APIs allow developers to read colors, typography, and other properties from the Material 3 theme, making it easier to create custom components. How to use Material Design 3 in Angular 18 To use Material Design 3 in Angular 18, you can define a theme in your application's styles.scss file using the mat.defineTheme function: ` Or generate a Material 3 theme using the Ng CLI: ` You can then apply the theme to your application using the mat.theme mixin: ` Head to the official guide for a more detailed guide. You'll also notice they have refreshed the docs with the new themes and further documentation. 4. Signal-Based APIs The path to fully signal-based components includes new signal inputs, model inputs, and signal query APIs. We already wrote about them as they were in developer-preview in v17, but they have been further refined in v18. These APIs offer a type-safe, reactive way to manage state changes and interactions within components: 1. Signal Input API: Signal inputs allow values to be bound from parent to child components. Those values are exposed using a signal and can change during the component's life cycle. 2. Model Input API: Model inputs are a special input type that enables a component to propagate new values back to the parent component. That allows developers to keep the parent component in sync with the child component with two-way binding. 3. Signal Query API: This was a particularly requested feature from the community. The signal query APIs work the same way as ViewChild and ContentChild under the hood, but they return signals, providing more predictable timing and type safety. 5. Fallback Content For ng-content A very requested feature from the community, Angular 18 introduces a new ng-content directive that allows developers to define fallback content when no content is projected into a component. This feature is particularly useful for creating reusable components with default content. Here's an example of using the new ng-content directive. Using the following component ` like this ` will render Howdy World. 6. Other Improvements In addition to the major updates mentioned above, Angular 18 also includes several other improvements and updates: 1. TypeScript 5.4: Angular 18 now supports TypeScript 5.4, which lets you take advantage of new features such as preserved narrowing in closures following last assignments. 2. Global Observable in Angular Forms: Angular 18 introduces a global events observable in Angular forms, which allows you to track all changes around any abstract control and its children, including the touched or dirty in a single observable. Here's an example of how you can use the global observable: ` 3. Stable Deferrable views: Deferrable views are now stable in Angular 18. 4. Stable Control Flow: The built-in control flow is now stable in Angular 18! It is more performant than its predecessor. It also received improved type checking, including guardrails for certain performance-related anti-patterns. 5. Route Redirects as Functions: For added flexibility in managing redirects, Angular v18 now lets you use a function for redirectTo that returns a string, which allows you to create more sophisticated redirection logic based on runtime conditions. For example: ` Conclusion Angular 18 is a significant release that brings many new features, enhancements, and experimental APIs to the Angular ecosystem. The introduction of zoneless change detection, improvements to server-side rendering and hydration, stable Material Design 3 support, signal-based APIs, and fallback content for ng-content are just a few of the highlights of this release. The Angular team has again demonstrated its commitment to improving the framework's developer experience, performance, and flexibility. It also demonstrated a clear vision for Angular's future. If you're curious about what's next, you can check out the Angular roadmap....

Overview of the New Signal APIs in Angular cover image

Overview of the New Signal APIs in Angular

Overview of the New Signal APIs in Angular Google's Minko Gechev and Jeremy Elbourn announced many exciting things at NG Conf 2024. Among them is the addition of several new signal-based APIs. They are already in developer preview, so we can play around with them. Let's dig into it, starting with signal-based inputs and the new matching outputs API. Signal Based Inputs Discussions about signal-based inputs have been taking place in the Angular community for some time now, and they are finally here. Until now, you used the @Input() decorator to define inputs. This is what you'd have to write in your component to declare an optional and a required input: ` With the new signal-based inputs, you can write much less boilerplate code. Here is how you can define the same inputs using the new syntax: ` It's not only less boilerplate, but because the values are signals, you can also use them directly in computed signals and effects. That, effectively, means you get to avoid computing combined values in ngOnChanges or using setters for your inputs to be able to compute derived values. In addition, input signals are read-only. The New Output I intentionally avoid calling them signal-based outputs because they are not. They still work the same way as the old outputs. The Angular team has just introduced a new output API that is more consistent with the latest inputs API and allows you to write less boilerplate, similar to the new input API. Here is how you would define an output until now: ` Here is how you can define the same output using the new syntax: ` The thing I like about the new output API is that sometimes it happens to me that I forget to instantiate the EventEmitter because I do this instead: ` You won't forget to instantiate the output with the new syntax because the output function does it for you. Signal Queries I am sure most readers know the @ViewChild, @ViewChildren, @ContentChild, and @ContentChildren decorators very well and have experienced the pain of triggering the infamous ExpressionChangedAfterItHasBeenCheckedError or having the values unavailable when needed. Here is a refresher on how you would use these decorators until now: ` With the new signal queries, similar to the new input API, the values are signals, and you can use them directly in computed signals and effects. You can define the same queries using the new syntax: ` Jeremy Elbourn mentioned that the new signal queries have better type inference and are more consistent with the new input and output APIs. He also showcased a brand new feature not available with the old queries. You can now define a query as required, and the Angular compiler will throw an error if the query has no result, guaranteeing that the value won't be undefined. Here is how you can define a required query: ` Model Inputs Jeremy and Minko announced the last new feature is the model inputs. The name is vague, but the feature is cool—it simplifies the definition of two-way bindings. Until now, to achieve two-way binding, you would have to define an input and an output following a given naming convention. @Input and @Output had to be defined with the same name (followed by "Change" in the case of the output). Then, you could use the template's [()] syntax. ` ` That way, you could keep the value in sync between the parent and the child component. With the new model inputs, you can define a two-way binding with a single line of code. Here is how you can define the same two-way binding using the new syntax: ` The html template stays the same: ` The model function returns a writable signal that can be updated directly. The value will be propagated back to any two-way bindings. Conclusion The new signal-based APIs are a great addition to Angular. They allow you to write less boilerplate code and make your components more reactive. The new APIs are already in developer preview, so you can start playing around with them today. I look forward to seeing how the community will adopt these new features and what other exciting things the Angular team has in store for us, such as zoneless apps by default....

Vercel & React Native - A New Era of Mobile Development? cover image

Vercel & React Native - A New Era of Mobile Development?

Vercel & React Native - A New Era of Mobile Development? Jared Palmer of Vercel recently announced an acquisition that spiked our interest. Having worked extensively with both Next.js and Vercel, as well as React Native, we were curious to see what the appointment of Fernando Rojo, the creator of Solito, as Vercel's Head of Mobile, would mean for the future of React Native and Vercel. While we can only speculate on what the future holds, we can look closer at Solito and its current integration with Vercel. Based on the information available, we can also make some educated guesses about what the future might hold for React Native and Vercel. What is Solito? Based on a recent tweet by Guillermo Rauch, one might assume that Solito allows you to build mobile apps with Next.js. While that might become a reality in the future, Jamon Holmgren, the CTO of Infinite Red, added some context to the conversation. According to Jamon, Solito is a cross-platform framework built on top of two existing technologies: - For the web, Solito leverages Next.js. - For mobile, Solito takes advantage of Expo. That means that, at the moment, you can't build mobile apps using Next.js & Solito only - you still need Expo and React Native. Even Jamon, however, admits that even the current integration of Solito with Vercel is exciting. Let's take a closer look at what Solito is according to its official website: > This library is two things: > > 1. A tiny wrapper around React Navigation and Next.js that lets you share navigation code across platforms. > > 2. A set of patterns and examples for building cross-platform apps with React Native + Next.js. We can see that Jamon was right - Solito allows you to share navigation code between Next.js and React Native and provides some patterns and components that you can use to build cross-platform apps, but it doesn't replace React Native or Expo. The Cross-Platformness of Solito So, we know Solito provides a way to share navigation and some patterns between Next.js and React Native. But what precisely does that entail? Cross-Platform Hooks and Components If you look at Solito's documentation, you'll see that it's not only navigation you can share between Next.js and React Native. There are a few components that wrap Next.js components and make them available in React Native: - Link - a component that wraps Next.js' Link component and allows you to navigate between screens in React Native. - TextLink - a component that also wraps Next.js' Link component but accepts text nodes as children. - MotiLink - a component that wraps Next.js' Link component and allows you to animate the link using moti, a popular animation library for React Native. - SolitoImage - a component that wraps Next.js' Image component and allows you to display images in React Native. On top of that, Solito provides a few hooks that you can use for shared routing and navigation: - useRouter() - a hook that lets you navigate between screens across platforms using URLs and Next.js Url objects. - useLink() - a hook that lets you create Link components across the two platforms. - createParam() - a function that returns the useParam() and useParams() hooks which allow you to access and update URL parameters across platforms. Shared Logic The Solito starter project is structured as a monorepo containing: - apps/next - the Next.js application. - apps/expo or apps/native - the React Native application. - packages/app - shared packages across the two applications: - features - providers - navigation The shared packages contain the shared logic and components you can use across the two platforms. For example, the features package contains the shared components organized by feature, the providers package contains the shared context providers, and the navigation package includes the shared navigation logic. One of the key principles of Solito is gradual adoption, meaning that if you use Solito and follow the recommended structure and patterns, you can start with a Next.js application only and eventually add a React Native application to the mix. Deployments Deploying the Next.js application built on Solito is as easy as deploying any other Next.js application. You can deploy it to Vercel like any other Next.js application, e.g., by linking your GitHub repository to Vercel and setting up automatic deployments. Deploying the React Native application built on top of Solito to Expo is a little bit more involved - you cannot directly use the Github Action recommended by Expo without some modification as Solito uses a monorepo structure. The adjustment, however, is luckily just a one-liner. You just need to add the working-directory parameter to the eas update --auto command in the Github Action. Here's what the modified part of the Expo Github Action would look like: ` What Does the Future Hold? While we can't predict the future, we can make some educated guesses about what the future might hold for Solito, React Native, Expo, and Vercel, given what we know about the current state of Solito and the recent acquisition of Fernando Rojo by Vercel. A Competitor to Expo? One question that comes to mind is whether Vercel will work towards creating a competitor to Expo. While it's too early to tell, it's not entirely out of the question. Vercel has been expanding its offering beyond Next.js and static sites, and it's not hard to imagine that it might want to provide a more integrated, frictionless solution for building mobile apps, further bridging the gap between web and mobile development. However, Expo is a mature and well-established platform, and building a mobile app toolchain from scratch is no trivial task. It would be easier for Vercel to build on top of Expo and partner with them to provide a more integrated solution for building mobile apps with Next.js. Furthermore, we need to consider Vercel's target audience. Most of Vercel's customers are focused on web development with Next.js, and switching to a mobile-first approach might not be in their best interest. That being said, Vercel has been expanding its offering to cater to a broader audience, and providing a more integrated solution for building mobile apps might be a step in that direction. A Cross-Platform Framework for Mobile Apps with Next.js? Imagine a future where you write your entire application in Next.js — using its routing, file structure, and dev tools — and still produce native mobile apps for iOS and Android. It's unlikely such functionality would be built from scratch. It would likely still rely on React Native + Expo to handle the actual native modules, build processes, and distribution. From the developer’s point of view, however, it would still feel like writing Next.js. While this idea sounds exciting, it's not likely to happen in the near future. Building a cross-platform framework that allows you to build mobile apps with Next.js only would require a lot of work and coordination between Vercel, Expo, and the React Native community. Furthermore, there are some conceptual differences between Next.js and React Native that would need to be addressed, such as Next.js being primarily SSR-oriented and native mobile apps running on the client. Vercel Building on Top of Solito? One of the more likely scenarios is that Vercel will build on top of Solito to provide a more integrated solution for building mobile apps with Next.js. This could involve providing more components, hooks, and patterns for building cross-platform apps, as well as improving the deployment process for React Native applications built on top of Solito. A potential partnership between Vercel and Expo, or at least some kind of closer integration, could also be in the cards in this scenario. While Expo already provides a robust infrastructure for building mobile apps, Vercel could provide complementary services or features that make it easier to build mobile apps on top of Solito. Conclusion Some news regarding Vercel and mobile development is very likely on the horizon. After all, Guillermo Rauch, the CEO of Vercel, has himself stated that Vercel will keep raising the quality bar of the mobile and web ecosystems. While it's unlikely we'll see a full-fledged mobile app framework built on top of Next.js or a direct competitor to Expo in the near future, it's not hard to imagine that Vercel will provide more tools and services for building mobile apps with Next.js. Solito is a step in that direction, and it's exciting to see what the future holds for mobile development with Vercel....

What does it actually look like to build software with AI today? Not in theory, but in practice. cover image

What does it actually look like to build software with AI today? Not in theory, but in practice.

What does it actually look like to build software with AI today? Not in theory, but in practice. At the Leadership Exchange, this was the question at the center of the Developer Panel, where leaders from across the industry unpacked what’s really changing inside engineering teams and what organizations need to do right now to keep up. The Developer Panel at the Leadership Exchange explored the cutting edge of AI in software engineering and examined what organizations should focus on today to prepare for the future. Moderated by Jeff Cross, Co-Founder & CEO at Nx, the panel featured Victor Savkin, Cofounder & CTO at Nx, Alex Sover, Vice President of Engineering at OpenAP, Brent Zucker, Senior Director of Engineering at Visa, and Jonathan Fontanez, AI Engineering Lead at This Dot Labs. Panelists shared insights into how AI is transforming the software development lifecycle and how teams can adopt tools effectively while preparing for organizational change. Panelists discussed emerging workflows, including CI-in-the-loop, agentic healing, and context engineering. They examined how validation, code reviews, and PRDs are evolving alongside AI capabilities and how teams are integrating external sources such as production traces to improve quality and reliability. The discussion also covered what the next generation of agentic tools might look like and how these capabilities will shape engineering practices in the near future. Adoption of AI comes with challenges. Teams often rely on plugins or extensions without foundational understanding, and individual contributors may fear displacement. Panelists emphasized that education, governance, and skill-building are essential for teams to manage AI agents effectively while maintaining quality. They also highlighted the need to standardize workflows and ensure organizational alignment to fully leverage AI capabilities. The conversation extended beyond technical challenges to organizational implications. Panelists discussed how teams can avoid issues like Conway’s Law, manage distributed teams effectively, and evolve engineering practices alongside AI adoption. Leadership and management strategies play a crucial role in ensuring that AI integration delivers meaningful outcomes while maintaining efficiency and alignment with business objectives. Key Takeaways - AI workflows require both technical and organizational preparation. - Education, governance, and skill development are essential for successful implementation. - Forward-looking teams are rethinking validation, CI pipelines, and context management to fully leverage agentic AI. The discussion highlighted that adopting AI at the cutting edge is not just about new tools - it is about rethinking processes, workflows, and organizational culture. Companies that embrace this holistic approach are most likely to succeed in leveraging AI to its full potential. Are you interested in more conversations like this? Message us for an invite to the next, or for a private discussion around these topics. Tracy can be reached at tlee@thisdot.co....

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co