Build Luigi App with React
Create a mock shopping application with React and configure it using Luigi.
Overview
You will learn
- How to add content to your React web shopping app
- How to use Luigi to configure navigation
- How to create simple micro-frontends using React
Prerequisites
Steps
In this step, you will create a file with information about the products on sale in your shopping app.
In a real life implementation, this data would be provided by an external service/API. But for simplicity, you will create a .js file containing dummy data. This file will provide the displayed data in the micro-frontend. You will create a similar file in the UI5 micro-frontend to avoid bundling issues.
Navigate to
react-core-mf/src/assetsand create aproducts.jsfile with the following content:JavaScriptexport const ProductCollection = [ { "id": 101, "name": "Mouse", "price": 45.0, "stock": 80, "icon": "product", "currencyCode": "EUR", "orderQuantity": 2, "description": "Wireless Gaming Mouse with Sensor" }, { "id": 102, "name": "Keyboard", "price": 50.0, "stock": 22, "icon": "product", "currencyCode": "EUR", "orderQuantity": 1, "description": "A physical keyboard that uses an individual spring and switch for each key. Today, only premium keyboards are built with key switches." }, { "id": 103, "name": "Optical Mouse", "price": 35.0, "stock": 4, "icon": "product", "currencyCode": "EUR", "orderQuantity": 2, "description": "Utilizing the latest optical sensing technology, the USB Optical Scroll Mouse records precise motion." }, { "id": 104, "name": "Laptop Pro", "price": 1299.0, "stock": 11, "icon": "laptop", "currencyCode": "EUR", "orderQuantity": 3, "description": "Newest laptop featuring a touch-sensitive OLED display." }, { "id": 105, "name": "Mouse 2", "price": 40.0, "stock": 20, "icon": "product", "currencyCode": "EUR", "orderQuantity": 6, "description": "The Mouse 2 is a computer mouse featuring a multi-touch acrylic surface for scrolling. The mouse features a lithium-ion rechargeable battery and Lightning connector for charging and pairing." }, { "id": 106, "name": "Printer", "price": 235.0, "stock": 24, "icon": "fx", "currencyCode": "EUR", "orderQuantity": 1, "description": "Affordable printer providing you with the optimal way to take care of all your printing needs." }, { "id": 107, "name": "Phone 11", "price": 835.0, "stock": 45, "icon": "iphone", "currencyCode": "EUR", "orderQuantity": 8, "description": "The Phone 11 dimensions are 150.9mm x 75.7mm x 8.3mm (H x W x D). It weighs about 194 grams (6.84 ounces)." }, { "id": 108, "name": "Phone 3a", "price": 299.0, "stock": 54, "icon": "desktop-mobile", "currencyCode": "EUR", "orderQuantity": 7, "description": "At 5.6 inches, the display is proportionate to the relatively small body of the phone." }, { "id": 109, "name": "Game Console 4", "price": 330.0, "stock": 94, "icon": "video", "currencyCode": "EUR", "orderQuantity": 1, "description": "This is the fourth home video game console compatible with all gaming systems." }, { "id": 110, "name": "Monitor", "price": 630.0, "stock": 20, "icon": "sys-monitor", "currencyCode": "EUR", "orderQuantity": 3, "description": "34'' Monitor, Display with stand Height adjustable (115 mm), tiltable (-5ยฐ to 21ยฐ), rotatable (-30ยฐ to 30ยฐ) Security slot (cable lock sold separately), anti-theft slot for locking to stand (for display). Includes: DisplayPort cable, HDMI cable, Power cable, Stand, USB 3.0 Type-A to Type-B cable, USB-C cable." }
]; ```
In this step, you will let Luigi take control of the index.html file - the entry point for your app.
Go to
react-core-mf/public/index.htmland change its content to:HTML<!DOCTYPE html> <html> <head> <title>Luigi</title> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1" /> <link rel="stylesheet" href="/luigi-core/luigi.css" /> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> <script src="/luigi-core/luigi.js"></script> <script src="/luigi-config.js"></script> </body> </html>
In this step, you will create another HTML file which will serve as a template for React to create the React micro-frontends.
Go to
react-core-mf/publicand create a new file calledsampleapp.html. Paste this code into the file:HTML<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>React App</title> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"></div> </body> </html>
In this step, you will configure webpack and adjust dependencies in order to make it easier to develop and build the app.
Go to
react-core-mf/webpack.config.js. Note that by default thewebpack-dev-serverwill redirect all requests toindex.htmlwhich is the app’s main entry point where the Luigi Core library is injected. However, since the micro-frontends are also built in the same environment, it’s necessary to host the root React micro-frontend app inside another file, in this case namedsampleapp.html. This has been done in lines 40-16 and a similar approach can be chosen with any other frontend framework:In
webpack.config.jsaround line 80, add a Webpack rule to allow importing CSS in the React project:JavaScriptmodule: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: [ ['@babel/preset-env', { targets: 'defaults' }], ['@babel/preset-react', { runtime: 'automatic' }] ] } } }, /// <---Add this rule --> { test: /\.(css)$/, use: ['style-loader', 'css-loader'], }, /// <-----> ] },
With the help of simple parameters pertaining to navigation and general settings, you will create your first “Home” navigation node and make your application responsive.
These are the Luigi navigation parameters you will use:
pathSegment- text segment added to the URLlabel- the name of the node displayed in the navigationicon- a SAP icon shown next to the labelviewUrl- the URL of your micro-frontend
Go to the file
react-core-mf/public/luigi-config.js. This is where you can find the Luigi configuration. Copy and paste this code inside it:JavaScriptvar defaultLocale = "en-US"; function myTranslationProvider() { var dict = { "en-US": { PRODUCTS: "Products", ORDERHISTORY: "Order History" }, }; return { getTranslation: function (label, interpolation, locale) { const local = locale || Luigi.i18n().getCurrentLocale() || defaultLocale var translations = dict[local] || dict[defaultLocale]; return (translations && translations[label]) || label; }, }; } Luigi.setConfig({ navigation: { nodes: () => [ { pathSegment: "home", label: "Home", icon: "home", viewUrl: "/sampleapp.html#/microfrontend/home", }, ], }, settings: { header: { title: "Luigi React App"}, responsiveNavigation: "simpleMobileOnly", customTranslationImplementation: myTranslationProvider, }, lifecycleHooks: { luigiAfterInit: () => { Luigi.i18n().setCurrentLocale(defaultLocale); }, }, communication: { customMessagesListeners: { "set-language": (msg) => { Luigi.i18n().setCurrentLocale(msg.locale); }, }, }, });
In this step, you will make changes to the entry point index.js for the React app. You will create a new view called home.js in react-core-mf/src/views, configure the router for this view, and import the Luigi Client. You will also create a file called language.js which will be useful for the next tutorials dealing with localization.
Open
react-core-mf/src/index.jsand change its content to:JavaScriptimport React, { useState, useEffect } from 'react'; import { createRoot } from 'react-dom/client'; import { HashRouter as Router, Routes, Route } from 'react-router-dom'; import Home from './views/home.js'; import { dict } from './language.js'; import { addInitListener, addContextUpdateListener, removeContextUpdateListener, removeInitListener, uxManager} from '@luigi-project/client'; import { ThemeProvider } from "@ui5/webcomponents-react"; const container = document.getElementById('root'); const root = createRoot(container); const App = () => { const [currentLocale, setCurrentLocale] = useState('en-US'); const [initListener, setInitListener] = useState(null); const [contextUpdateListener, setContextUpdateListener] = useState(null); useEffect(() => { const updateCurrentLanguage = () => { setCurrentLocale(uxManager().getCurrentLocale()) } setInitListener( addInitListener(() => { console.log("Luigi Client initialized."); // update current language upon Luigi Client initialization updateCurrentLanguage(); }) ); setContextUpdateListener( addContextUpdateListener(() => { // update current language upon Luigi Client context update event updateCurrentLanguage(); }) ); return function cleanup() { removeContextUpdateListener(contextUpdateListener); removeInitListener(initListener); }; }, []); return ( <ThemeProvider> <React.StrictMode> <Router basename="microfrontend"> <Routes> <Route path="/home" element={<Home localeDict={dict[currentLocale]} currentLocale={currentLocale} />} /> </Routes> </Router> </React.StrictMode> </ThemeProvider> ); }; root.render(<App />);Next, go to the
react-core-mf/src/viewsdirectory created in step 6 of the previous tutorial. Find the file calledhome.jsand paste the following code into it:JavaScriptimport React, { useState, useEffect } from 'react'; import "fundamental-styles/dist/fundamental-styles.css"; import { addInitListener, addContextUpdateListener, removeContextUpdateListener, removeInitListener, sendCustomMessage } from "@luigi-project/client"; import { Grid, Panel, Select, Option } from "@ui5/webcomponents-react"; const Home = (props) => { const [options] = useState([{ key: 'en-US', text: 'en-US' }]); function onChangeValue(event) { sendCustomMessage({ id: "set-language", locale: event.detail.selectedOption.innerText, }); } return ( <Grid position="Center" defaultIndent="XL1 L1 M1 S1" defaultSpan="XL10 L10 M10 S10"> <Panel headerText={props.localeDict.WELCOME_LUIGI} headerLevel="H3"> <Select onChange={onChangeValue}> {options.map((language) => ( <Option key={language.key}>{language.text}</Option> ))} </Select> </Panel> </Grid> ); }; export default Home;Create a new file in
react-core-mf/srccalledlanguage.jswith the following content:JavaScriptexport const dict = { "en-US": { ITEMS: "Products", STOCKS: "Stocks", SELECTLANGUAGE: "Please select a language", PRICE: "Price", WELCOME_LUIGI: "Welcome to Luigi - a micro-frontend framework", DESCRIPTION: "Description", PRODUCTADDED: "Product has been added to cart", AVAILABLE: "Available", AVAILABLEQUANT: "Available quantity: ", ADDTOCART: "Add to cart", BACK: "Back", OUTOFSTOCK: "Out of stock", }, };
In this step, you will add a navigation node in Luigi for the “Products” micro-frontend.
Open
react-core-mf/public/luigi-config.jsAdd a new “Products” node to your navigation:
JavaScriptnavigation: { nodes: () => [ { pathSegment: "home", label: "Home", icon: "home", viewUrl: "/sampleapp.html#/microfrontend/home", //<---Add the section below to the config file---> children: [ { pathSegment: "products", label: "Products", icon: "product", viewUrl: "/sampleapp.html#/microfrontend/products", } ], // <------> }, ], },Next, rename the file
sample1.jsinreact-core-mf/src/viewstoproducts.jsand paste following code into it:JavaScriptimport React, { useEffect, useState } from 'react'; import "../../node_modules/fundamental-styles/dist/fundamental-styles.css"; import "@ui5/webcomponents-icons/dist/AllIcons.js"; import { Grid, List, ListItemStandard } from "@ui5/webcomponents-react"; import { linkManager } from "@luigi-project/client"; import { ProductCollection } from "../assets/products.js"; const Products = (props) => { const [listItems, setListItems] = useState([]); useEffect(() => { const tempList = []; ProductCollection.forEach((product) => { tempList.push( <ListItemStandard id={product.id} key={product.id} additionalText={product.price + " " + product.currencyCode} additionalTextState="Information" description={product.description} icon={product.icon} type="Active" onItemClick={() => handleItemClick(product.id)}> {product.name} </ListItemStandard> ); setListItems(tempList); }); }, []) // navigates to productDetail microfrontend through Luigi Client linkManager API function handleItemClick(event) { linkManager().withParams({ root: "products" }); linkManager().navigate( "/home/products/" + event.detail.item.id.toString() ); }; return ( <Grid position="Center" defaultIndent="XL1 L1 M1 S1" defaultSpan="XL10 L10 M10 S10"> <List headerText={props.localeDict.ITEMS + ": " + ProductCollection.length} onItemClick={handleItemClick}> {listItems} </List> </Grid> ); }; export default Products;Add the routing module to the
index.jsby adding the following lines to the file:JavaScript//<---Around line 8, paste this line: --->: import Products from "./views/products.js"; //<------> ... <Router basename="microfrontend"> <Routes> <Route path="/home" render={(props) => <Home {...props} localeDict={dict[this.state.currentLocale]} currentLocale={this.state.currentLocale} />} /> //<---Around line 45, paste this line: ---> <Route path="/products" element={<Products localeDict={dict[currentLocale]} />} /> //<------> </Routes> </Router>
In this step, you will add the ProductDetail view to the app. You will be able to show details for each product via a Luigi dynamic parameter, in this case named :id.
- In
react-core-mf/public/luigi-config.jsadd a child node:idto theproductsnode:
```JavaScript
children: [
{
pathSegment: "products",
label: "Products",
icon: "product",
viewUrl: "/sampleapp.html#/microfrontend/products",
//<---Paste the section below to your config:--->
keepSelectedForChildren: true,
children: [{
pathSegment: ':id',
viewUrl: '/sampleapp.html#/microfrontend/productDetail/:id',
context: { id: ':id' }
}]
//<------>
},
```
Next, create a new file in
react-core-mf/src/viewsnamedproductDetail.jsand paste following content into it:JavaScriptimport React, { useEffect, useState, useRef } from 'react'; import "../../node_modules/fundamental-styles/dist/fundamental-styles.css"; import "@ui5/webcomponents-icons/dist/AllIcons.js"; import { Grid, ObjectPage, Label, DynamicPageHeader, DynamicPageTitle, ObjectStatus, FlexBox, Button, Toast, ObjectPageSection, FormItem, Form, Text, Bar } from "@ui5/webcomponents-react"; import { linkManager, getContext } from "@luigi-project/client"; import { ProductCollection } from "../assets/products.js"; const ProductDetail = (props) => { const [currentProduct, setCurrentProduct] = useState({}); const [availability, setAvailability] = useState({ state: "Warning", text: props.localeDict.OUTOFSTOCK }); const toast = useRef(null); useEffect(() => { setProductAndAvailability(); }, []); function setProductAndAvailability() { // get id from Luigi Client getContext API const id = getContext().id; let product = ProductCollection.find( (product) => product.id.toString() === id ) || ProductCollection[0] setCurrentProduct(product); currentProduct.stock ? setAvailability({ state: "Success", text: props.localeDict.AVAILABLE }) : "" } // Use Luigi Client linkManager API to navigate to the previous microfrontend function navBack() { // checks if there is a previous view in history if (linkManager().hasBack()) { // navigates to the previously opened microfrontend linkManager().goBack(); } else { // navigates to the products page directly linkManager().navigate("/home/products"); } }; return ( <Grid position="Center" defaultIndent="XL1 L1 M1 S1" defaultSpan="XL10 L10 M10 S10"> <ObjectPage headerContent={ <DynamicPageHeader> <FlexBox alignItems="Center" wrap="Wrap"> <FlexBox direction="Column" style={{ padding: "10px" }}> <Label> {props.localeDict.AVAILABLEQUANT + currentProduct.stock} </Label> </FlexBox> </FlexBox> </DynamicPageHeader> } footer={ <Bar design="FloatingFooter" endContent={ <> <Button design="Emphasized" onClick={() => { toast.current.show() }}> {props.localeDict.ADDTOCART} </Button> <Button design="Default" onClick={navBack}> {props.localeDict.BACK} </Button> </> } /> } headerContentPinnable headerTitle={ <DynamicPageTitle actions={ <> <Toast ref={toast}> {props.localeDict.PRODUCTADDED} </Toast> <Button design="Emphasized" onClick={() => { toast.current.show() }}> {props.localeDict.ADDTOCART} </Button> <Button onClick={navBack}> {props.localeDict.BACK} </Button> </> } header={currentProduct.name} > <ObjectStatus state={availability.state}> {availability.text} </ObjectStatus> </DynamicPageTitle> } onSelectedSectionChange={function noRefCheck() { }} selectedSectionId="details" showHideHeaderButton style={{ height: "700px", }} > <ObjectPageSection aria-label="Details" id="details" titleText="Details"> <Form columnsL={2} columnsM={2} columnsXL={3} labelSpanL={1} labelSpanM={1} labelSpanXL={1}> <FormItem label="Name"> <Text>{currentProduct.name}</Text> </FormItem> <FormItem label={props.localeDict.DESCRIPTION}> <Text>{currentProduct.description}</Text> </FormItem> <FormItem label={props.localeDict.PRICE}> <Text> {currentProduct.price + " " + currentProduct.currencyCode} </Text> </FormItem> </Form> </ObjectPageSection> </ObjectPage> </Grid> ); }; export default ProductDetail;Add the route configuration to the
index.jsby pasting below code into it:JavaScript//<---Around line 9, paste this line:---> import ProductDetail from './views/productDetail'; //<------> ... <Router basename="microfrontend"> <Routes> <Route path="/home" render={(props) => <Home {...props} localeDict={dict[this.state.currentLocale]} currentLocale={this.state.currentLocale} />} /> <Route path="/products" render={(props) => <Products {...props} localeDict={dict[this.state.currentLocale]} />} /> //<---Around line 50, paste this line:---> <Route path="/productDetail/:id" element={<ProductDetail localeDict={dict[currentLocale]} />} /> //<------> </Routes> </Router>
npm i @ui5/webcomponents-fioriIn this step, you can check if your core app is configured correctly so far by running it locally.
Open a terminal/command prompt window. Navigate to the
react-core-mffolder.Input
npm start. Your application should be up and running athttp://localhost:3000/. You should be able to see the homepage and “Products” view. You can also navigate toward theProductDetailview via “Products”.
Resources
Discussion
Share feedback on this tutorial or join the conversation in SAP Community.