Using JSX in Apostrophe
In modern web development, tools and technologies evolve rapidly, and so do the demands of web applications. ApostropheCMS offers a robust and flexible platform for building content-rich websites, it comes with a powerful built-in Webpack build system that caters to most development needs out of the box. This system streamlines the process of managing assets, optimizing performance, and ensuring a smooth developer experience. However, there are times when you may want to extend its capabilities by customizing the build process. One common scenario is integrating React components into your ApostropheCMS project, which involves customizing Webpack to support JSX (JavaScript XML). By leveraging JSX and React, you can enhance the interactivity and maintainability of your front-end components, providing a richer user experience while still taking advantage of the features offered by ApostropheCMS.
Why Customize Your Webpack Build?
Webpack is a powerful module bundler that compiles JavaScript modules into a single file or multiple files that the browser can understand. Customizing your Webpack configuration can offer several benefits:
- Enhanced Development Workflow: Customizing Webpack allows you to integrate modern JavaScript frameworks like React, enabling a more component-based architecture.
- Performance Optimization: By customizing Webpack, you can take advantage of advanced features such as code splitting, tree shaking, and caching to optimize the performance of your application.
- Extended Functionality: Webpack's plugin system allows you to extend its functionality to handle various types of assets (e.g., images, fonts, SVGs) and preprocessors (e.g., Babel for modern JavaScript syntax).
- Improved Maintainability: A customized Webpack build can help maintain a cleaner and more modular codebase, making it easier to manage and scale your project.
Advantages of Using JSX for a Dynamic Component
While Nunjucks is a powerful templating engine for server-side rendering in ApostropheCMS, using JSX with React offers several advantages for building interactive and dynamic user interfaces:
- Component-Based Architecture: JSX allows you to build reusable components, encapsulating both the markup and logic. This modularity makes it easier to manage complex UIs and promotes code reusability.
- State Management: React's state management capabilities enable you to handle dynamic data changes efficiently. This means you can easily manage and update the state as new data is fetched.
- Enhanced Interactivity: With React and JSX, you can create highly interactive UIs with real-time updates and smooth user experiences, such as automatically updating elements without a full page reload.
Building a Weather App with JSX
In this tutorial, we'll walk through the process of customizing your Webpack configuration to support JSX in an ApostropheCMS project. We'll build a weather widget that leverages the power of React components for a dynamic and interactive user interface. The code for this widget is based on a basic React tutorial that you can find here. By the end of this tutorial, you'll understand how to set up a custom Webpack build and take advantage of JSX to enhance your ApostropheCMS projects.
Adding the Weather Widget to your Project
We will start this tutorial by creating a new widget in an already created starter kit project using the Apostrophe CLI tool. At the root of your project, run the following on the command line:
apos add widget react-weather-widget --player
Next, add the new widget to the app.js
file.
You can choose to add this widget to any area, but for this tutorial we will add it to the default page-type.
Adding JSX to Our Project
Now that we have our widget added, we will turn our attention to modifying the project Webpack configuration. A typical Webpack configuration is organized into several key sections that define how different types of files should be processed and managed. Within an ApostropheCMS project, we typically modify three configuration sections:
- Module: Specifies rules for handling different file types through loaders.
- Plugins: Allows for the inclusion of plugins that perform a wide range of tasks, from optimizing bundles to injecting environment variables.
- Resolve: Helps Webpack understand how to locate and bundle modules by specifying file extensions and aliasing module paths.
The existing Apostrophe Webpack build uses the Babel compiler to allow the use of modern JavaScript while supporting older browsers. In this case, we will be extending the module
section to recognize and transpile JSX files by adding a new object to the rules
array. Open the modules/react-weather-widget/index.js
and add the following:
INFO
This new rule will be merged by Apostrophe into the existing array of rules, allowing you to create rules in multiple project-level modules.
Within the new rule, we are adding a test
property with a regular expression to determine if the rule should be applied to a file. In this case, we are using the loader for files with either the .js
or .jsx
extensions. It is a matter of preference whether you want other developers to be able to use the .js
extension for JSX files. We are also adding an exclude
property so that the files in the node_modules
folder aren't processed.
In the use
section, we state that the file should be loaded using the babel-loader
. This Webpack loader will let us use Babel presets and plugins to transpile our .jsx
files. In this case, we are using the @babel/preset-react
preset to interpret our JSX.
In addition to providing a new module rule, we also need to tell the Webpack build that files with a .jsx
extension should be run through the build process. This is done by extending the resolve
section's extensions
array.
In order for our new Webpack build to function, we need to add the new development dependencies. Navigate to the root of your project in your terminal and issue the following command:
npm install babel-loader @babel/preset-react --save-dev
Creating the Weather App Component
Now that we are able to use JSX in our project, we need to create a component that utilizes it. At the moment, we have only modified our project to be able to transpile JSX files. We haven't changed the build entry point. That means that all of our app component files should be placed into the custom module ui/src
folder and be imported through the index.js
file located in that folder. That file is also going to act to bootstrap our app.
At the top of this file we are importing both react
and the createRoot
function from react-dom/client
. This will allow us to use the React framework in our project. We are also importing the main entry point App
. In this case I'm electing to add that file and the other component files inside the ui/src
folder, but you can elect to place them anywhere inside your project, as long as you import them through the ui/src/index.js
file. To use these two packages we need to add them to our project dependencies. Since they are being used on the front-end, not during the Webpack build, we need to add them as regular dependencies. Navigate to the root of your project in your terminal and issue the following command: npm install react react-dom/client
.
The remainder of this file is a standard widget player. This player is attached to the [data-react-weather-widget]
attribute that we will need to add to the widget Nunjucks template. Within that element, it selects an element with an id of react-weather-root
to create the root for our React component. We are also passing a prop we are getting from the data-default-city
attribute on our rootElement
. We will need to set the value of this attribute using the data passed to the template from the widget schema.
Adding the widget Nunjucks template
The markup for this widget on the Nunjucks side is going to be simple. We require an attribute for the player to identify the code our client-side JavaScript player should target, a target where React will render our component root, and another attribute for passing data between the widget schema fields and the react app.
Briefly, the attribute on the section
tag is what we are passing into the selector
property of the player. This section contains a single div
element that will be used as the root. Finally, on that same element we are setting the data-default-city
attribute value to data passed from the widget defaultCity
schema field, or an empty string if the content manager hasn't added a string to that field.
Modifying the widget schema fields
We have already added our Webpack configuration changes to the modules/react-weather-widget/index.js
file, but now we also want to add the defaultCity
schema field.
As we will see when we cover the JSX code files, this default city will cause the widget to be prepopulated with data from a selected city that can then be replaced with user input.
Adding the main App.jsx
component
Since this tutorial is mainly focused on how you use React in an ApostropheCMS project, we aren't going to go through the fine points of the React code we are adding.
It should be noted that the two components used by this React app are being imported in the App.jsx
file that is imported in the base ui/src/index.js
file. The Webpack build is clever enough to import all the files without having to import them to the base, as long as they are imported into a file that is imported into the base. The only other part of this code we need to focus on is the fetchWeather()
function. In this app we have elected to use the OpenWeatherMap API to retrieve the weather for each city. At the time of this writing it had a generous free tier, and easy geolocation from a city name. However, it does require an API key. We don't want to directly add this key into our App.jsx
code since it will be exposed client-side. Instead, we are going to create a proxy endpoint in our project that will fetch the data and pass it back to our component.
const response = await fetch(
'/api/v1/react-weather-widget/fetch-weather?' +
new URLSearchParams({
city: cityName
})
);
This line in that function performs a fetch on the /fetch-weather
endpoint, passing in the city name as a parameter.
There are several ways we can add endpoints to an ApostropheCMS project. In this case we are using the apiRoutes(self)
customization function. This code creates a single GET
route that can be accessed at the URL /api/v1/react-weather-widget/fetch-weather
. Note that the function name automatically gets converted to kebab case, so fetchWeather
becomes fetch-weather
. If the function name for the route starts with a slash, we would use that directly when we are calling it from our components. This is useful when you need a public facing URL.
The remainder of this code should be fairly self-explanatory. We are getting the city
value from the request object and the API key from the environment variable that should be passed when starting our project, OPENWEATHERMAP_API_KEY=XXXXXX npm run dev
.
Next the function passes this information to the Open Weather Map API and gets back data that is returned to the component.
Creating the CityComponent
component
Again, we aren't going to focus on most of the JSX component code.
We have already installed react
as a dependency of our project, but we are also utilizing the styled-components
package in this component. Again, this will be front-end, so it should be a normal, not development dependency. Navigate to the root of your project in your terminal and issue the following command: npm install styled-components
.
The one line of code that needs to be addressed in an ApostropheCMS project is the import of the icon this component uses: import PerfectDay from '../icons/perfect-day.svg';
. While the @apostrophecms/attachment
module will allow the upload of files with an svg
extension, These files won't be included in the bundled code sent to the front-end. To facilitate image access like you would experience in a React app, we are going to further modify our Webpack configuration and add all of our icons to the modules/react-weather-widget/ui/src/icons
folder.
To import the files into our Webpack build, we also have to make a modification to the project Webpack configuration. Open the modules/react-weather-widget/index.js
and make the following modifications:
We are adding a new rules
object that specifies that any files with an svg
extension use the file-loader
to be brought into the project bundle. The extensions
array of the resolve
section also needs to be modified to allow for processing of files with the .svg
extension. We have to install this loader in our project by running the command npm install file-loader --save-dev
on the command line at the root of our project.
Creating the WeatherComponent
component
Again, we won't touch much on the JSX code.
As with the CityComponent.jsx
file, we are importing react
and styled-components
packages. We are also importing five SVG weather info icons from the modules/react-weather-widget/ui/src/icons
folder. The OpenWeatherMap site makes the remainder of the images we need available on their site.
Conclusions
In this tutorial, we covered the basics of how to create a widget powered by React and JSX components. Similar steps can be used to allow you to use Vue, Svelte, or Angular components in your project. You need to identify the correct loader(s) for the file types you want to use, add any presets to transpile the files, and make sure that the Webpack build is screening files with the expected extensions.
For this widget, we only added a single render root. But to add additional components, we simply need to make sure that each element passed from the DOM to the createRoot()
function is unique. Whether it is passed through a widget player, added as a fragment, or directly into the Nunjucks template. Note that if you are adding front-end JavaScript to create and render your root element outside a widget player, make sure to wrap your script in an apos.util.onReady()
listener so that it triggers a rerender when the page content is updated during editing.