Upgrade JSS apps to JSS 22.10

Version: 22.x

Sitecore JSS version 22.10 primarily focuses on support for Angular 20. For Angular apps, you will need to update multiple dependencies. In general, the only other requirement is to update and install core JSS dependencies before re-testing your apps.

Before you begin
  • If you haven't already done so, upgrade your app to JSS 22.9.

  • Since this process also involves updating Angular to version 20, you'll need to refer to the official guide to complete the upgrade process for your apps.

  • Familiarize yourself with the JSS 22.10 changelog to ensure you understand the implications of upgrading.

This topic describes how to:

Update the Angular template files in your existing app

This version of JSS upgrades Angular to version 20 that switches your components to be standalone by default. You should refer to the official Angular guide to complete the upgrade process for your apps and familiarize with standalone components and relevant migrations.

To upgrade your JSS app to Angular 20:

  1. In your existing application's package.json file, update the following dependencies to the specified versions:

    RequestResponse
    "@angular/animations": "~20.3.10",
    "@angular/common": "~20.3.10",
    "@angular/compiler": "~20.3.10",
    "@angular/core": "~20.3.10",
    "@angular/forms": "~20.3.10",
    "@angular/platform-browser": "~20.3.10",
    "@angular/platform-browser-dynamic": "~20.3.10",
    "@angular/platform-server": "~20.3.10",
    "@angular/router": "~20.3.10",
    ....
    "apollo-angular": "~11.0.0",
    ...
    "graphql": "~16.11.0",
    ...
    "zone.js": "~0.15.0",
    ...
    "@angular-builders/custom-webpack": "^20.0.0",
    "@angular-devkit/build-angular": "^20.3.9",
    "@angular-eslint/builder": "^20.5.1",
    "@angular-eslint/eslint-plugin": "^20.5.1",
    "@angular-eslint/eslint-plugin-template": "^20.5.1",
    "@angular-eslint/schematics": "^20.5.1",
    "@angular-eslint/template-parser": "^20.5.1",
    "@angular/cli": "~20.3.9",
    "@angular/compiler-cli": "~20.3.10",
    "@angular/language-service": "~20.3.10",
    ...
    "typescript": "~5.8.0"
  2. In the src/app folder:

    • Add a new file called ViewBag.ts with the following content:

      RequestResponse
      export interface ViewBag {
          [key: string]: unknown;
          dictionary: { [key: string]: string };
      }
    • Add a new file called injection-tokens.ts with the following content:

      RequestResponse
      import { InjectionToken } from '@angular/core';
      import { InMemoryCache } from '@apollo/client/core';
      import { JssState } from './JssState';
      import { ViewBag } from './ViewBag';
      
      /**
       * Injection token for server-side layout data provided by server.bundle
       */
      export const JSS_SERVER_LAYOUT_DATA = new InjectionToken<JssState>('JSS_SERVER_LAYOUT_DATA');
      
      export const JSS_SERVER_VIEWBAG = new InjectionToken<ViewBag>('JSS_SERVER_VIEWBAG');
      
      export const APOLLO_CACHE = new InjectionToken<InMemoryCache>('APOLLO_CACHE');
  3. Use the inject() method to to replace field initializations in the constructor for all instances of class fields.

    For example, in src/app/jss-context.service.ts, the original constructor is as shown:

    RequestResponse
    constructor(protected transferState: TransferState, protected layoutService: JssLayoutService, protected stateService: JssStateService<JssState>) {}

    To use the inject() method here:

    • Import the method from the angular package:

      RequestResponse
      import { Injectable, TransferState, makeStateKey, inject } from '@angular/core';
    • Use the inject() function to remove the field initializations from the constructor as shown:

      RequestResponse
      protected transferState = inject(TransferState);
      protected layoutService = inject(JssLayoutService);
      protected stateService = inject(JssStateService<JssState>);
    • Repeat the above steps for the rest of the app logic like components, services (for example, src/app/jss-context.server.ts, jss-graphql.service.ts files), etc.

  4. For all the modules using declarations, move the entries into imports instead.

    • For example, in src/app/routing/routing.module.ts, the existing declarations are as shown:

      RequestResponse
      @NgModule({
        imports: [
          ....
        ],
        exports: [...],
        declarations: [NotFoundComponent, ServerErrorComponent, LayoutComponent, NavigationComponent],

      Move the entries to the imports as shown:

      RequestResponse
      @NgModule({
        imports: [
          ...
          LayoutComponent,
          NotFoundComponent,
          ServerErrorComponent,
        ],
        exports: [RouterModule, TranslateModule],
        ...
      })
    • Similarly, in src/app/routing/scripts/scripts.module.ts, move the ScriptsComponent and VisitorIdentificationComponent (if present) from declarations to imports as shown:

      RequestResponse
      @NgModule({
        exports: [ScriptsComponent],
        imports: [ScriptsComponent, VisitorIdentificationComponent],
      })
    • Perform the same step for StyleguideSpecimenComponent in src/app/components/app-components.shared.module.ts and any other component modules you wish to have as standalone.

  5. The app.component.ts file will remain non-standalone for simplicity and continued support of SSR proxy. Make the following changes to the app.component.ts file:

    • Mark the component with standalone: false as shown

      RequestResponse
      @Component({
        selector: 'app-root',
        templateUrl: './app.component.html'
        templateUrl: './app.component.html',
        /* eslint-disable-next-line @angular-eslint/prefer-standalone */
        standalone: false,
      })
    • Implement the OnInit interface

      RequestResponse
      export class AppComponent implements OnInit, OnDestroy {
    • Add the ngInit() method:

      RequestResponse
      ngOnInit() {
      this.contextSubscription = this.jssContextService.state.subscribe((jssState: { language: string }) => {
          // listen for language changes
          if (jssState.language) {
          this.translate.use(jssState.language);
          }
      });
      }
    • Remove the original constructor and initialize fields with the inject() method:

      RequestResponse
      ...
      private contextSubscription: Subscription;
      private translate = inject(TranslateService);
      private jssContextService = inject(JssContextService);
      ...
    • Making the app-component non-standalone allows you to keep the declarations section in app.module.ts as is, but app.server.module.ts should be modified to use an injection token.

      Find the existing string token:

      RequestResponse
      TranslateModule.forRoot({
            ....
              deps: ['JSS_SERVER_VIEWBAG', TransferState],
            ....
          }),

      Replace it with an imported token object:

      RequestResponse
      import { JSS_SERVER_VIEWBAG } from './src/app/injection-tokens';
      ....
      ....
      TranslateModule.forRoot({
            ...
              deps: [JSS_SERVER_VIEWBAG, TransferState],
            ...
          }),
  6. Use injection tokens in the server.bundle.ts file:

    • Import the injection tokens:

      RequestResponse
      import { JSS_SERVER_LAYOUT_DATA, JSS_SERVER_VIEWBAG } from './src/app/injection-tokens';
    • Add the injection tokens to the extraProviders section of the renderModule function as shown:

      RequestResponse
      renderModule(AppServerModule, {
            document: template,
            url: path,
            extraProviders: [
              // custom injection with the initial state that SSR should utilize
              { provide: JSS_SERVER_LAYOUT_DATA, useValue: transferState },
              { provide: JSS_SERVER_VIEWBAG, useValue: state.viewBag },
            ],
          })
  7. In the app's src/app/routing/layout/layout.component.ts file:

    • Ensure the following imports are present:

      RequestResponse
      import { Component, OnInit, OnDestroy, inject } from '@angular/core';
      import {
        RouteData,
        Field,
        LayoutServiceContextData,
        getContentStylesheetLink,
        JssModule,
      } from '@sitecore-jss/sitecore-jss-angular';
      ...
      import { NotFoundComponent } from '../not-found/not-found.component';
      import { ServerErrorComponent } from '../server-error/server-error.component';
      import { ScriptsComponent } from '../scripts/scripts.component';
      ...
      import { CommonModule } from '@angular/common';
    • Add an imports section for the Component directive:

      RequestResponse
      @Component({
        selector: 'app-layout',
        templateUrl: './layout.component.html',
        imports: [CommonModule, JssModule, NotFoundComponent, ServerErrorComponent, ScriptsComponent],
      })
    • Use the inject() function to remove the field initializations from the constructor as shown:

      RequestResponse
      private activatedRoute = inject(ActivatedRoute);
      private readonly meta = inject(JssMetaService);
      private linkService = inject(JssLinkService);
  8. In the app's jss-graphql.module.ts file:

    • Import inject from the angular package, replace ApolloModule with provideApollo from the apollo-angular package, and import the APOLLO_CACHE injection token as shown:

      RequestResponse
      import { makeStateKey, TransferState, NgModule, PLATFORM_ID, inject } from '@angular/core';
      ....
      import { Apollo, provideApollo } from 'apollo-angular';
      ...
      import { APOLLO_CACHE } from './injection-tokens';
    • Add the following two new function outside of GraphQLModule:

      RequestResponse
      const getGraphQLCache = () => {
        const possibleTypes = {} as PossibleTypesMap;
        introspectionQueryResultData.__schema.types.forEach((supertype) => {
          possibleTypes[supertype.name] = supertype.possibleTypes.map((subtype) => subtype.name);
        });
        return new InMemoryCache({
          possibleTypes,
        });
      };
      
      const getApolloOptions = () => {
        const httpLink = inject(HttpBatchLink);
        const platformId = inject(PLATFORM_ID);
        /*
        QUERY LINK SELECTION
        A link is transport which GraphQL queries are pushed across.
        You have many choices.
        See the apollo-link documentation for more details.
      */
      
        // set sc_apikey header which is required for any GraphQL calls
        const sc_apikey = new HttpHeaders().set('sc_apikey', environment.sitecoreApiKey);
      
        // choose between a basic HTTP link to run queries...
        // import { createHttpLink } from 'apollo-angular-link-http';
        // const link = createHttpLink({ uri: endpoint });
      
        // ...or a batched link (multiple queries within 10ms all go in one HTTP request)
        const batchHttp = httpLink.create({
          uri: environment.graphQLEndpoint,
          headers: sc_apikey,
        });
      
        const cache = inject(APOLLO_CACHE);
      
        return {
          link: batchHttp,
          cache,
          ssrMode: isPlatformServer(platformId),
          ssrForceFetchDelay: 100,
        };
      }
    • Remove ApolloModule from imports and expand the providers section as shown:

      RequestResponse
      @NgModule({
        imports: [
          HttpClientModule, // provides HttpClient for HttpLink
        ],
        providers: [
          JssGraphQLService,
          { provide: APOLLO_CACHE, useValue: getGraphQLCache() },
          provideApollo(getApolloOptions),
        ],
      })
    • Modify the module constructor as shown:

      RequestResponse
      constructor() {
        this.addCacheEvents();
      }
    • Initialize two module fields with the inject() function:

      RequestResponse
      private readonly transferState = inject(TransferState);
      apolloClient = inject(Apollo);
    • Replace the createApolloClient method with the following:

      RequestResponse
      private addCacheEvents(): void {
          const cache = inject(APOLLO_CACHE);
      
          const isBrowser = this.transferState.hasKey(STATE_KEY);
      
          if (isBrowser) {
              this.onBrowser(cache);
          } else {
              this.onServer(cache);
          }
      }
  9. In the app's src/app/jss-graphql.service.ts file, use the inject() function to remove the field initializations from the constructor as shown:

    RequestResponse
    protected apollo = inject(Apollo);
    protected sitecoreContext = inject(JssContextService);
    protected platformId = inject(PLATFORM_ID);
  10. You can optionally replace all usages of ngIf and ngFor in your components with the new @if and @for directives. See the updated Angular sample for examples on how to do it. To continue using NgIf and NgFor with your standalone components, import ngIf and ngFor from the @angular/core package and add them to your component’s imports section:

    RequestResponse
    @Component({
      ...
      imports: [NgIf, NgFor, ....],
    })
    export class MyComponent implements .... {...

Update application dependencies in your existing app

For your upgraded application to work correctly, you will need to update dependencies.

To update your dependencies:

  1. In your existing application's package.json file, update every @sitecore-jss package to version ~22.10.0.

  2. Install the dependencies with the following command:

    RequestResponse
    npm install

Do you have some feedback for us?

If you have suggestions for improving this article,