How to use Sitecore Helix principles in a headless application (React JSS and NextJS)

Posted 02/23/2022 by Albraa Nabelsi

Sitecore Helix is a set of official guidelines and recommended practices for Sitecore Development. it provides a set of architecture conventions and guidelines that describe how to apply recommended technical design principles to a Sitecore project. The purpose is to secure implementations in a future-proof way by architecting them as maintainable and extensible business-centric modules. While the Helix architecture is a great way to organize a Sitecore solution, the principles can be applied to JSS apps too. In this post, we will be using a standard React JSS app to demonstrate how Helix principles can be used in a headless application, but this approach works well with other frameworks too.

To start, let us examine the structure of a standard JSS app:

All of the components are listed directly under the src/components/ folder. To apply Helix architecture, we will move the components to the following structure:

We did the following:

  1. Created Foundation, Feature, and Project folders under the src/components/ folder.
  2. Created folders for the ‘features’ under the src/components/Feature folder.
    1. Moved the GraphQL and Styleguide components to their own ‘feature’ folders
    2. Moved the other custom components to the ‘Demo’ folder.

Let us test the app now:

The components no longer work on the app. This is because the React component is not registered in the generated componentFactory.js file.

The componentFactory.js file is generated by the scripts/generate-component-factory.js file. Open the file and inspect the generateComponentFactory function, notice that it is expecting all of the components directly under the src/components/ folder. We will need to modify the file to account for the new helix structure.

We have modified the code to the following:

const fs = require('fs');
const path = require('path');
const chokidar = require('chokidar');

/*
  COMPONENT FACTORY GENERATION
  Generates the /src/temp/componentFactory.js file which maps React components
  to JSS components.

  The component factory is a mapping between a string name and a React component instance.
  When the Sitecore Layout service returns a layout definition, it returns named components.
  This mapping is used to construct the component hierarchy for the layout.

  The default convention uses the parent folder name as the component name,
  but it is customizable in generateComponentFactory().

  NOTE: this script can run in two modes. The default mode, the component factory file is written once.
  But if `--watch` is a process argument, the component factory source folder will be watched,
  and the componentFactory.js rewritten on added or deleted files.
  This is used during `jss start` to pick up new or removed components at runtime.
*/

/* eslint-disable no-console */

const componentFactoryPath = path.resolve('src/temp/componentFactory.js');
const componentRootPath = 'src/components';

const isWatch = process.argv.some((arg) => arg === '--watch');

if (isWatch) {
  watchComponentFactory();
} else {
  writeComponentFactory();
}

function watchComponentFactory() {
  console.log(`Watching for changes to component factory sources in ${componentRootPath}...`);

  chokidar
    .watch(componentRootPath, { ignoreInitial: true, awaitWriteFinish: true })
    .on('add', writeComponentFactory)
    .on('unlink', writeComponentFactory);
}

function writeComponentFactory() {
  const componentFactory = generateComponentFactory();

  console.log(`Writing component factory to ${componentFactoryPath}`);

  fs.writeFileSync(componentFactoryPath, componentFactory, { encoding: 'utf8' });
}

function generateComponentFactory() {
  // by convention, we expect to find React components
  // * under /src/components/ComponentName
  // * with an index.js under the folder to define the component
  // If you'd like to use your own convention, encode it below.
  // NOTE: generating the component factory is also totally optional,
  // and it can be maintained manually if preferred.

  const imports = [];
  const registrations = [];

  fs.readdirSync(componentRootPath).forEach((srcSubFolder) => {
    if (srcSubFolder !== 'Feature' && srcSubFolder !== 'Project') return;
    const srcSubFolderFullPath = path.join(componentRootPath, srcSubFolder);

    fs.readdirSync(srcSubFolderFullPath).forEach((moduleFolder) => {
      const moduleComponentsFolderFullPath = path.join(srcSubFolderFullPath, moduleFolder);
      if (!fs.existsSync(moduleComponentsFolderFullPath)) return;

      fs.readdirSync(moduleComponentsFolderFullPath).forEach((componentFolder) => {
        const componentFolderFullPath = path.join(moduleComponentsFolderFullPath, componentFolder);
        if (
          !fs.existsSync(path.join(componentFolderFullPath, 'index.js')) &&
          !fs.existsSync(path.join(componentFolderFullPath, 'index.jsx'))
        ) {
          return;
        }

        const importVarName = componentFolder.replace(/[^\w]+/g, '');

        console.debug(`Registering JSS component ${componentFolder}`);
        imports.push(
          `import ${importVarName} from '../components/${srcSubFolder}/${moduleFolder}/${componentFolder}';`
        );
        registrations.push(`components.set('${componentFolder}', ${importVarName});`);
      });
    });
  });

  return `/* eslint-disable */
// Do not edit this file, it is auto-generated at build time!
// See scripts/generate-component-factory.js to modify the generation of this file.
${imports.join('\n')}

const components = new Map();
${registrations.join('\n')}

export default function componentFactory(componentName) {
  return components.get(componentName);
};
`;
}

Now let's start the app again:

The components appear now!

For NextJS

For Next.JS apps, the process is a bit simpler, we start by moving the components to the Helix structure like before:

We don’t have to update the scripts/generate-component-factory.ts file. Take a look at the code. The getComponentList function does not care about the structure.

After we move the components, we can start the app and see that the components are still working: