CRA+Craft : How to use Create React App in a Craft CMS multiple pages site
Tags: craftcms, cra, webpack, react
Published: 2020-10-07 18:08:12


TL;DR: Customize Create React App to use it as a frontend foundation for a multiple page site powered by Craft CMS.

----

There are a lot of articles that already cover how to use [Craft CMS](https://craftcms.com) with React but they focus mainly on the usage of Graphql. In this post we will see how to use the great [Create React App](https://create-react-app.dev) with a template, in conjunction with Craft CMS, to get the best of both worlds: a community optimized React app, battle-tested front-end tools, real multiple entry points with different bundle per pages.

We’ll be going together throughout all the process, there is quite a journey ahead of us, so let’s get started.
> I recommend to have at least basic knowledges about Craft CMS, PHP, CRA, and a good comprehension of Webpack

1. [Installing CRA with a template](#step1)
2. [Installing Craft CMS](#step2)
3. [Customize CRA using craco](#step3)
4. [Change dev and build paths](#step4)
5. [Add support for multiple entry points](#step5)
6. [Add Tailwind and tweak Postcss](#step6)
7. [Add another plugin](#step7)
8. [Injecting the js and css files associated with each template when necessary](#step8)

## Installing CRA with a template <a name="step1"></a>

Since v3.3 react-scripts support [templates](https://create-react-app.dev/docs/custom-templates/), we’ll use the wonderful [react boilerplate template](https://github.com/react-boilerplate/react-boilerplate) especially the [v5-with-cra branch](https://github.com/react-boilerplate/react-boilerplate/tree/v5-with-cra) which add Typescript support, but it’s not mandatory, you can use [any templates](https://www.npmjs.com/search?q=cra-template-*&ranking=optimal), or no template at all, depending on your needs, anyway lets continue:
> For what follows our project will be named **cracraft**, and **CRA** refer to _Create react app_

```bash
$ npx create-react-app --template cra-template-rb cracraft
```

{% collapsible Fix _A template was not provided error_ %}

If you ever come across this error:

```
A template was not provided. This is likely because you're using an outdated version of create-react-app.
Please note that global installs of create-react-app are no longer supported.
```

First, remove all globally installed version of CRA:
- with `npm uninstall -g create-react-app`
- or `yarn global remove create-react-app`

Then if yarn gives you this: `error This module isn't specified in a package.json file.` don't trust him blindly, run:
```bash
$ readlink which `create-react-app`
```
And if you get a path, it’s another version of CRA package remaining in your system; so remove it, and try again.

{% endcollapsible %}

Once installed, cd into the directory and run `npm start` to make sure that everything is running smoothly.

## Installing Craft CMS <a name="step2"></a>

Installing Craft CMS is quite simple, thanks to [composer](https://getcomposer.org/), but there is a small problem: we can’t install it in a non-empty directory, so we should do it in two steps:
- first install craft in a temporary directory `composer create-project craftcms/craft cracraft-temp`
- then, once finished, move all files in the directory where we installed CRA previously and delete the temporary `cracraft-temp` directory

You can now follow the rest of the installation process here: https://craftcms.com/docs/3.x/installation.html#step-2-set-the-file-permissions

The purpose of this setup is not only to integrate CRA in a Twig template, it will do the job for a single page application, but for a website with multiple pages, where some page could actually contain elaborate widgets or complex applications, and others just need a few lines of javascript or no javascript at all... we need more flexibility.

But as well designed as CRA is, it is not really flexible, and it makes sense, because it was made to be a SPA: there is only one html page and you inject the whole bundle or nothing. At this point, we now have on one side a multi-page site powered by Craft CMS and on the other side a SPA powered by CRA, we need to fusion the two.

<p align="center">
  <img src="https://media.giphy.com/media/iDIZIQQfAJG9VgWciQ/giphy-downsized.gif">
</p>

## Customize CRA using craco <a name="step3"></a>

Let’s customize CRA to play well with Craft CMS, the goal is to tweak the Webpack configuration without ejecting, and thus keep the advantages of being able to update either CRA or the template.
There are a couple of options for customization:
* [Create React App Configuration Override](https://github.com/gsoft-inc/craco)
* [Rescripts](https://github.com/harrysolovay/rescripts)
* [React app rewired](https://github.com/timarney/react-app-rewired)
* Forking React scripts

Craco has my preference, because I like the way it handles the tweaking of different parts, it exposes the Webpack configuration and we can overwrite almost anything we want. Add it to the project:
```bash
$ npm install @craco/craco --save
```
Next, create in the root directory, a file that will contains all of our modifications, name it `craco.config.js`.

And finally update the _start_ and _build_ script command to use craco instead of react-scripts.

In **package.json**
```json
"scripts": {
  "start": "craco start",
  "build": "craco build",
  "test": "craco test",
  ...
},
```
> If you want to know more about the full details of the craco installation, it’s [here](https://github.com/gsoft-inc/craco/blob/master/packages/craco/README.md#installation)

That’s it, time to write some code.

At this point we want to:
- Change some dev and build paths to match our Craft folder structure
- Set up multiple entry points to be able to inject different bundle in different pages
- Add and modify Webpack plugins
- Add Tailwind and tweak Postcss
- Retrieve the js and css files associated with each pages when needed

We will share variables between CRA and Craft CMS, the easiest way is to add them in the existing `.env` file:
```bash
WDS_SOCKET_HOST=localhost
WDS_SOCKET_PORT=3000
PUBLIC_PATH="http://localhost:3000/"
MANIFEST_PATH=/asset-manifest.json
FAST_REFRESH=true
```
> FAST_REFRESH enable [react-refresh](https://www.npmjs.com/package/react-refresh), it’s not mandatory to enable it, but it’s allow to improve components updates

### Change dev and build paths <a name="step4"></a>

Assuming that the `src` directory contains all the javascript and styles sources, and that we want to output the result of the build step in `web/dist`:
```bash
cracraft
├── src
│   ├── styles
│   │   ├── **/*.css
│   ├── app
│   │   ├── **/*.tsx
│   ├── js
│   │   ├── **/*.js
│   └── ...
├── web
│   ├── dist
│   └── ...
├── templates
├── craco.config.js
├── .env
└── ...
```

We have to tell Webpack where our files are and where we want the output, both for dev and build mode:

In **craco.config.js**
```js
const {
  whenDev,
  whenProd,
} = require('@craco/craco');

module.exports = function ({ env }) {
  return {
    webpack: {
      configure: (webpackConfig, { env, paths }) => {
        whenDev(() => {
          webpackConfig.output.publicPath = process.env.PUBLIC_PATH;
        });

        whenProd(() => {
          const buildPath = `${paths.appPath}/web${process.env.PUBLIC_PATH}`;

          paths.appBuild = buildPath;
          webpackConfig.output.path = buildPath;
          webpackConfig.output.publicPath = process.env.PUBLIC_PATH;

          return webpackConfig;
        });
      }
    },
    plugins: [],
    style: {}
  }
}
```

{% collapsible Using a local domain like .local  %}

To avoid CORS errors between your local domain and the dev server, add a header to the webpack dev server, using craco’s ability to change the configuration of the dev server.

In **craco.config.js**
```js
...
plugins: [
  {
    plugin: {
      overrideDevServerConfig: ({
        devServerConfig,
        cracoConfig,
        pluginOptions,
        context: { env, paths, proxy, allowedHost },
      }) => {
        devServerConfig.headers = {
          'Access-Control-Allow-Origin': '*',
        };
        return devServerConfig;
      },
    }
  },
],
...
```
{% endcollapsible %}

### Add support for multiple entry points <a name="step5"></a>

CRA doesn’t support multiple entry points out of the box, so we have to reconfigure Webpack to add some. Let’s say we have 3 different pages:
* _home_ where we want to use Typescript
* _editor_ a page containing the react SPA
* _about_ a page that need only a simple javascript snippet

> NB: Dont forget to create these pages and their associated Twig templates

In **craco.config.js** add our 3 entry points
```js
...
module.exports = function ({ env }) {
  return {
    webpack: {
      configure: (webpackConfig, { env, paths }) => {
        const entries = {
          index: [...webpackConfig.entry],
          home: [`${paths.appSrc}/js/home.ts`],
          about: [`${paths.appSrc}/js/about/index.js`],
        };
        ...
      }
    }
  }
}
```
> Note: We keep index as name for the entry point of the react SPA

It won’t work yet, because the _ManifestPlugin_ already used in CRA will cause a problem, it is [configured](https://github.com/facebook/create-react-app/issues/1084#issuecomment-598516292) to support a single entry point. And in order to overwrite the config of a Webpack plugin we need to replace it.

Install the plugin:
```bash
$ npm i ManifestPlugin -D
```
Create a new instance of the plugin, and replace the existing one in the plugin array:

In **craco.config.js**
```js
...
module.exports = function ({ env }) {
  return {
    webpack: {
      configure: (webpackConfig, { env, paths }) => {
        ...
        // Substitute ManifestPlugin:
        const pluginPosition = webpackConfig.plugins.findIndex(
          ({ constructor }) => constructor.name === 'ManifestPlugin',
        );

        const multipleEntriesManifestPlugin = new ManifestPlugin({
          fileName: 'asset-manifest.json',
          publicPath: paths.publicUrlOrPath,
          generate: (seed, files, entrypoints) => {
            const manifestFiles = files.reduce((manifest, file) => {
              manifest[file.name] = file.path;
              return manifest;
            }, seed);

            // Keep the existing entry point
            const indexEntrypointFiles = entrypoints.index.filter(
              fileName => !fileName.endsWith('.map'),
            );

            let { index, ...pagesAllEntryPointFiles } = entrypoints;
            // Create our pages entry points
            const pagesEntryPointFiles = Object.keys(
              pagesAllEntryPointFiles,
            ).reduce((filtered, entryKey) => {
              filtered[entryKey] = pagesAllEntryPointFiles[entryKey].filter(
                fileName => !fileName.endsWith('.map'),
              );
              return filtered;
            }, {});

            return {
              files: manifestFiles,
              entrypoints: indexEntrypointFiles,
              pages: pagesEntryPointFiles,
            };
          },
        });

        webpackConfig.plugins.splice(
          pluginPosition,
          1,
          multipleEntriesManifestPlugin,
        );
        ...
      }
    }
  }
}
```

And hop! It’s done, we’ve just substituted the plugin.

We’re almost done, there is one step left to support our new entry points, we have to add the `webpackHotDevClient` for each one to support [HMR](https://webpack.js.org/guides/hot-module-replacement/#root)

In **craco.config.js**
```js
...
whenDev(() => {
  webpackConfig.output.publicPath = process.env.PUBLIC_PATH;
  webpackConfig.optimization.runtimeChunk = 'single';

  const webpackHotDevClientPath = require.resolve(
    'react-dev-utils/webpackHotDevClient',
  );
  Object.keys(entries).forEach(entryKey => {
    if (!entries[entryKey].includes(webpackHotDevClientPath)) {
      entries[entryKey].unshift(webpackHotDevClientPath);
    }
  });
});
...
```

{% collapsible Quick tips: debuging your customizations %}

If you customize CRA and come across a tricky bug, remember that you can still debug the process like any nodejs app by adding the `--inspect` flag to your npm script command `craco --inspect build`

{% endcollapsible %}

### Add Tailwind and tweak Postcss <a name="step6"></a>

Once everything is in place and the dev server plus the build step are running without any errors, we can customize further to integrate all our needs, for the demonstration we’ll add a favicon plugin, customize Postcss and use [Tailwind](https://tailwindcss.com/) css framework.

So first, Tailwind and Postcss, it’s quite straightforward, start by adding the necessary packages:
```bash
$ npm i -D postcss-import postcss-preset-env tailwindcss
```
In the root directory of the project create a `tailwind.config.js` file.

In **craco.config.js** add our Postcss configuration:
```js
...
style: {
  postcss: {
    plugins: [
      require('postcss-import')({
        plugins: [require('stylelint')],
        path: ['./node_modules'],
      }),
      require('tailwindcss')('./tailwind.config.js'),
      require('postcss-preset-env')({
        autoprefixer: {},
        features: {
          'nesting-rules': true,
        },
      })
    ],
  },
},
...
```

And to make it perfect, we need to tell Stylelint to chill out with some unorthodox rules used in Tailwind.
Add these rules to the `.stylelintrc` config file:
```js
"rules": {
    "at-rule-no-unknown": [ true, {
      "ignoreAtRules": [
        "screen",
        "extends",
        "responsive",
        "tailwind"
      ]
    }],
    "block-no-empty": null
  }
```

### Add another plugin <a name="step7"></a>

Next, add the Favicons Webpack Plugin, here it’s even simplier because we just have to push it in the Webpack config plugin array, provided by craco, like this:
```js
whenProd(() => {
  ...

  webpackConfig.plugins.push(
    new FaviconsWebpackPlugin({
      logo: './src/img/favicon-src.png',
      prefix: 'img/favicons/',
      cache: true,
      inject: 'force',
      favicons: {
        appName: 'Cracraft',
        appDescription: 'Create react app and Craft play well together',
        developerName: 'Dev name',
        developerURL: 'name@mail.dev',
        path: 'web/dist/',
      },
    }),
  );
});
```

> Note that when you add a plugin which is already used in the CRA config you have to replace it, using the same trick as the one used previously for the _ManifestPlugin_.

### Injecting the js and css files associated with each templates when necessary <a name="step8"></a>

Ooook now that CRA is customized, there is one last step to link it to Craft CMS: we need to retrieve the content of the different endpoints, and, since the manifest file is a plain json file, it’s easy to read it and get the parts we need.

How are we going to do that?
* Quick answer: this can be done with a Twig function
* Long answer: there is a better way to do it, but we’ll talk about it in another post, as this one is starting to be quite long (congrats if you're still reading from the beginning).

So, lets write a simple Twig function that will load our manifest file and create the HTML tags.

First install a PHP implementation of [JsonPath](https://github.com/Galbar/JsonPath-PHP)
```bash
$ composer require galbar/jsonpath
```

In the file where you declare your Twig extensions, import all the dependencies:
```PHP
use craft\helpers\Html;
use craft\helpers\Template;
use craft\helpers\Json as JsonHelper;
use JsonPath\JsonObject;
```
And add a function that will get the content of the manifest file and return the paths of the chunks we are looking for; let’s call it _getEntryPointChunks_, and it will take a `$jsonPath` param, I let you go through the code:
```PHP
public function getEntryPointChunks(string $path)
{
    $publicPath = getenv('PUBLIC_PATH');
    $manifestPath = getenv('MANIFEST_PATH');
    $manifestContent = file_get_contents($publicPath.$manifestPath);
    $manifest = JsonHelper::decodeIfJson($manifestContent);
    $jsonObject = new JsonObject($manifestContent);

    $moduleList = $jsonObject->get($jsonPath);

    if (!$moduleList) {
        return null;
    }

    // Ensure flat array, ex: if [*] is forgotten in the json path to an array
    if (is_array($moduleList)) {
        $flattened = [];
        array_walk_recursive($moduleList, function ($item) use (&$flattened) {
            $flattened[] = $item;
        });

        $moduleList = $flattened;
    }

    $moduleTags = [];
    foreach ($moduleList as $k => $m) {
        if (strpos($m, '.hot-update.js') === false) {
            $moduleName = preg_replace('/^\//', '', $m);
            if (preg_match('/\.css(\?.*)?$/i', $moduleName)) {
                $moduleTags[] = Html::cssFile("$publicPath/$moduleName");
            } elseif (preg_match('/\.js(\?.*)?$/i', $moduleName)) {
                $moduleTags[] = Html::jsFile("$publicPath/$moduleName");
            } elseif (preg_match('/\.(svg|png|jpe?g|webp|avif|gif)(\?.*)?$/i', $moduleName)) {
                $moduleTags[] = Html::img("$publicPath/$moduleName");
            } else {
                $moduleTags[] = "$publicPath/$moduleName";
            }
        }
    }

    return Template::raw(implode("\r\n", $moduleTags));
}
```
And now, finally, call our function from any twig template:
```twig
{# Inject the spa react app #}
{{getEntryPointChunks('$.entrypoints')}}
```
and for another page
```twig
{# Inject vanilla javascript #}
{{getEntryPointChunks('$.pages.about')}}
```

> NB: don’t forget to start the development server before accessing your Craft site `$ npm start`

----

And that’s it, this is the end, we have an (almost) complete fusion between CRA and Craft CMS with a multi entry points setup and a customizable webpack configuration.

<p align="center">
  <img src="https://media.giphy.com/media/Cgs9sEyhtNip2/giphy.gif">
</p>

Next time we will complete this setup with a better way to integrate all this with Craft CMS, because even though the Twig function does the job, there is room for improvement, for now enjoy this setup to harness the full power of CRA+Craft.

Thanks for reading, hope you learned something new and it will help you.