A brief introduction to the concept of a Nx React standalone application

Grzegorz Sroka
GumGum Tech Blog
Published in
14 min readMar 27, 2024

These days, there are numerous tools, frameworks, or libraries available to help you with the creation of applications. However, not all of them come with modern features out-of-the-box, and adding everything to your app can be time-consuming and cause a lot of problems.

That’s where the Nx build system comes in. Nx helps with simple configuration, so you don’t have to worry about installing all necessary dependancies individually. This way, you can start writing applications right away without the initial headache of configuring everything.

Examples of core Nx features include:

  • Built-in test runners,
  • Easy integration of new tools, like Storybook,
  • Ensure consistency and code quality with custom generators and lint rules,
  • Interactive workspace visualization,
  • Prettier configuration,
  • GitHub integration,
  • Caching.

In this post, I will show you how to create React applications using the Nx build system. With Nx, you can create both monorepo and standalone applications (either Angular or React; more at https://nx.dev/getting-started/intro). In my case, it will be a standalone React application because it will showcase the capabilities of Nx in a very small project.

Why Nx React standalone?

Nx React standalone provides several benefits for developing React applications:

  1. Clear architectural guidelines and best practices. Nx helps define efficient architectural guidelines for a maintainable, scalable React application.
  2. Adaptability: Nx enables you to start small with a single-project setup and grows it into a monorepo as needed.
  3. Build-in futures: Nx has built-in features that provide valuable tooling support for Standalone Applications.

In summary, using Nx React standalone provides a powerful and efficient development environment for building React applications, ensuring codebase maintainability, scalability, and integration with modern tooling.

standalone apps — for when you want a single project that can be nicely structured and modularized. It’s a good starting point if you’re not looking into a monorepo but with the option to expand later.

1. Project setup

In this example, we will create a simple Chatbot application. For the UI, we’ll use the Mantine library (https://mantine.dev), which helps us create views very quickly.

First, we’ll set up an Nx workspace and learn how to create a new project, libraries and components. We’ll also cover using the Nx Console plugin with Visual Studio Code and we will take a look how to plug in Mantine to our project.

Please note that this post require a basic knowledge of React.

2. Nx Workspace

Before we proceed, you need to have an Nx Workspace prepared. To create a standalone Nx React project, use the following command:

npx create-nx-workspace@latest nx-chat --preset=react-standalone

During the setup process, you will be asked for some additional information. You can choose options based on your preferences or follow the one I selected:

Create Nx Workspace

3. Nx Generate

From now on, you can use the generate command to create Nx application elements. This command allows you to generate various elements in your workspace, such as components, modules, libraries, and more.

Alternatively, you can generate application elements using Nx Console if that’s your preference (step 6).

For all the commands below to work, you must install Nx globally. If you don’t want to do this, make sure the commands you type are preceded by npx.

npm install --global nx@latest

Info: You can run all below commands with --dry-run added after command. Thanks to that, you will be able to see the command result in the terminal before it is executed.

Generate Library

Firstly, we will generate a library to show you how you can structure your application. The purpose of generating a React “library” with Nx is to create a reusable set of components, utilities, or services that can be shared across multiple projects within your Nx workspace.

By generating a library, Nx sets up the necessary files and folder structure for the library, including the component or utility files. This allows us to develop and maintain a collection of reusable code that can be easily imported and used in different applications within our workspace.

To generate a React library in Nx, you can use the following command:

cd nx-chat && nx generate @nx/react:library --name=src/app/chat --bundler=vite --compiler=swc --projectNameAndRootFormat=as-provided --style=scss --unitTestRunner=vitest --no-interactive

The command will create a new library named chat in your project, located inside the src/app folder. As before, you can choose settings based on your preferences or change them according to your needs. You can then add React components, utilities, or services within the generated files.

Nx Generate Library

Note: What is cool about it is when generating Nx elements, an alias is also generated. This way, we do not have to remember long paths, and the IDE will prompt us with the correct name. For example import { Chat } from ‘@nx-chat/chat’:

Generate Component

Now, let’s create a simple React component. This chat library catalogue will be our main folder for all of them.

The purpose of generating React “components” with Nx is to create reusable UI elements that can be used across different parts of your application. By generating them, Nx sets up the necessary files and folder structure , including the component file itself and any associated styles or tests. This allows you to develop and maintain a library of reusable UI components that can be easily imported and used in different parts of your application.

To keep things simple we will name our components header, navbar, main, and footer.

To generate a React component in Nx, you can use the following command:

nx g @nx/react:component --name=src/app/chat/src/lib/ui/header/header --nameAndDirectoryFormat=as-provided --style=scss

Fire this command for all other components that you want to create.

Nx Generate Component

Info: When creating big applications, it is a good practice to create a folder at the top of your application, for example, src/shared/ui, and keep there components that can be shared across all over the application or libraries.

4. Nx Serve

To serve your Nx project, you can use the following command:

nx serve

This command starts a development server and serves your project. By default, it will serve the main application in your workspace. After running the nx serve command, you can access your project in the browser at http://localhost:4200 (or a different port if specified).

nx serve

Info: If you have multiple applications in your workspace, you can specify the application name to serve a specific one nx serve nx-chat

5. Nx Build

The purpose of the nx build command is to trigger the building process for your project in an Nx workspace. It compiles the source code, bundles assets, and prepares the application for deployment. It optimizes the code, removes unnecessary files, and prepares the application for deployment to a web server or hosting platform.

This command will build the project:

nx build
nx build

6. Nx Console

Nx Console is a graphical user interface (GUI) for executing various commands and tasks within an Nx workspace. It offers a range of commands that can be accessed through the user interface. You can generate components directly from Nx Console, run, test, or build the app and many more.

Now let’s open our project in Visual Studio Code (Nx Console is also available for JetBrains). To install the Nx Console plugin, go to “Extensions” and look for “Nx Console”.

Nx Console

Generate & Run Target

Now that you have installed Nx Console, you can use it to generate application elements, run app, test application, and more directly from the user interface.

Nx Console Generate

Generate library

When generating by UI you have plenty of options that you can customize. In my case I chose few options that I changed from default, which would be:

  • name (it includes destination path): src/app/chat
  • bundler: vite
  • compiler: swc
  • style: scss
  • unitTestRunner: vitest

It is up to you if you want to set it up differently or customize more options. I named my library chat but it’s up to you how you call it, just replace LIBRARY_NAME with your own name. Be aware that before you press the Generate button, you can preview what is going to be generated in the IDE console. All changes will trigger a preview of the command. This will help to avoid possible mistakes.

Nx Console Generate Library

Generate Component

In the case of Components, there are fewer options to select because it is going to be a simple React component. Our choice will include style and test files:

  • name (it includes destination path): src/app/chat/src/lib/ui/header/header
  • style: scss
Nx Console Generate Component

Run application

Thanks to Nx Console, you can run commands directly from the IDE. This allows you to serve or build projects directly from the UI. You can also use many more commands. Using Nx Console, you can easily manage your projects within an Nx workspace and execute various tasks directly from the IDE, making your workflow more efficient and streamlined.

Nx Console / nx run nx-chat:serve

7. Chat library

Now let’s setup OpenAI library. I just want to quickly mention here about this. For more details please go to official website. Also keep in mind that you will need your own api key to make it work.

Setting up

First, install the openai package:

npm install --save openai

additionally install dependency that will allow to read .env file data. Dotenv is a zero-dependency module that loads environment variables from a .env file into process.env :

npm install --save dotenv

After installing dotenv to make it work you have to add this peace of code at the bottom of vite.config.ts file:

{  
...
define: {
'process.env': process.env,
},
}

Now, create a .env file in your project’s root directory and add your OpenAI API key:

OPENAI_API_KEY=sk-[YOUR_API_KEY]

Replace [YOUR_API_KEY] with your own actual API key. Be aware that I store api key like this only for the test purpose.

Code

You can create a file openai.api.ts in your chat directory or you can generate library with this Nx command:

npx nx generate @nx/js:library --name=src/app/chat/src/lib/utils/openai.api --bundler=vite --minimal=true --projectNameAndRootFormat=as-provided --unitTestRunner=vitest --no-interactive

Right now let’s create code that will make request to open AI.

// src/app/chat/src/lib/utils/openai.api/src/lib/openai.api.ts

import OpenAI from 'openai';

export interface ChatState {
model: 'gpt-3.5-turbo' | 'gpt-4';
temperature: number;
maxTokens: number;
topP: number;
frequencyPenalty: number;
presencePenalty: number;
}

export const initialMessage = {
text: 'Hello, I am a chatbot, how can I help you?',
isBot: true,
};

const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
dangerouslyAllowBrowser: true,
});

export async function sendMessageToOpenAI(
message: string,
{
model,
temperature,
maxTokens,
topP,
frequencyPenalty,
presencePenalty,
}: ChatState
) {
const stream = await openai.chat.completions.create({
model: model,
temperature: temperature,
max_tokens: maxTokens,
top_p: topP,
frequency_penalty: frequencyPenalty,
presence_penalty: presencePenalty,
messages: [{ role: 'user', content: message }],
});

return stream.choices[0].message.content;
}

8. Adding UI library

In our project, I will be using the Mantine library. It is a comprehensive library that includes numerous reusable components and hooks. Mantine supports all modern frameworks and is type-safe.

Build fully functional accessible web applications faster than ever — Mantine includes more than 100 customizable components and 50 hooks to cover you in any situation

Install dependencies

First, we need to install all the dependencies required for our project:

npm install @mantine/core @mantine/hooks

We will also use Tabler icons, which are supported by Mantine:

npm install @tabler/icons-react --save

Mantine provider

Now, let’s wrap our Nx app with the Mantine provider and add Mantine style import on the top of the file. Lets also insert our <Chat />library inside this provider. After doing this Mantine will be supported in our app.

app.tsx

Mantine UI

If this is your first encounter with Mantine, a good starting point, besides the documentation, is the Mantine UI website. There, you can find many structure examples that you can use when building your application.

Appshell

Now, let’s jump to our chat.tsx file. We will structure our app there. I choose my app to be organized with Mantine AppShell. You can copy the full code from the examples provided here. In my case, it will be a responsive appshell with header, navbar, main, and footer.

As the next step, let’s import the components that we generated before and add them to their designated places. Let’s also add the desired code:

// src/app/chat/src/lib/chat.tsx

import { useState } from 'react';
import { AppShell } from '@mantine/core';
import { useDisclosure, useScrollIntoView } from '@mantine/hooks';

import Header from './ui/header/header';
import Navbar from './ui/navbar/navbar';
import Main from './ui/main/main';
import Footer from './ui/footer/footer';

import { ChatState, sendMessageToOpenAI, initialMessage } from '@nx-chat/openai.api';

export function Chat() {
const { scrollIntoView, targetRef } = useScrollIntoView<HTMLDivElement>({
offset: 160,
});
const [mobileOpened, { toggle: toggleMobile }] = useDisclosure();
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
const [btnDisabled, setBtnDisabled] = useState(false);
const [chatMessageText, setChatMessageText] = useState('');
const [message, setMessage] = useState([initialMessage]);

const [chatState, setChatState] = useState<ChatState>({
model: 'gpt-3.5-turbo',
temperature: 0.5,
topP: 0.5,
maxTokens: 100,
frequencyPenalty: 0.5,
presencePenalty: 0.5,
});

const handleChatSend = async () => {
setBtnDisabled(true);
setChatMessageText('');
setMessage([
...message,
{
text: chatMessageText,
isBot: false,
},
]);

const res = await sendMessageToOpenAI(chatMessageText, chatState);

setMessage([
...message,
{
text: chatMessageText,
isBot: false,
},
{
text: res || '',
isBot: true,
},
] as { text: string; isBot: boolean }[]);
setBtnDisabled(false);
setTimeout(() => {
scrollIntoView({
alignment: 'end',
});
}, 10);
};

const handleChatReset = () => {
setMessage([initialMessage]);
};

const updateChatState = (newState: ChatState) => {
setChatState({ ...chatState, ...newState });
}

const updateChatMessageText = (chatMessageText: string) => {
setChatMessageText(chatMessageText);
}

return (
<AppShell
layout="alt"
header={{ height: 60 }}
navbar={{
width: 300,
breakpoint: 'sm',
collapsed: { mobile: !mobileOpened, desktop: !desktopOpened },
}}
footer={{ height: 120 }}
padding="md"
>
<AppShell.Header>
<Header mobileOpened={mobileOpened} desktopOpened={desktopOpened} toggleMobile={toggleMobile} toggleDesktop={toggleDesktop} />
</AppShell.Header>
<AppShell.Navbar>
<Navbar btnDisabled={btnDisabled} handleChatReset={handleChatReset} chatState={chatState} updateChatState={updateChatState} />
</AppShell.Navbar>
<AppShell.Main>
<Main btnDisabled={btnDisabled} targetRef={targetRef} message={message} />
</AppShell.Main>
<AppShell.Footer p="md">
<Footer chatMessageText={chatMessageText} handleChatSend={handleChatSend} updateChatMessageText={updateChatMessageText} />
</AppShell.Footer>
</AppShell>
);
}

export default Chat;

Header

It’s time to fill our components with some code. Our header component will be the most simple one. It will include only one action button responsible for closing and opening the navbar:

// src/app/chat/src/lib/ui/header/header.tsx

import { Burger, Group, Text, Title } from '@mantine/core';
import { MantineLogo } from '@mantinex/mantine-logo';

/* eslint-disable-next-line */
export interface HeaderProps {
mobileOpened: boolean;
desktopOpened: boolean;
toggleMobile: () => void;
toggleDesktop: () => void;
}

export function Header({ mobileOpened, desktopOpened, toggleMobile, toggleDesktop }: HeaderProps) {
return (
<Group h="100%" px="md">
<Burger
opened={mobileOpened}
onClick={toggleMobile}
hiddenFrom="sm"
size="sm"
/>
<Burger
opened={desktopOpened}
onClick={toggleDesktop}
visibleFrom="sm"
size="sm"
/>
<Group>
<MantineLogo color="blue" size={28} />
<Title order={1}>
<Text
size="xl"
fw={900}
variant="gradient"
gradient={{ from: 'blue', to: 'cyan', deg: 90 }}
>
Nx Chatbot
</Text>
</Title>
</Group>
</Group>
);
}

export default Header;

Navbar

The navbar component will contain sliders responsible for adjusting chat parameters. Thanks to them, we will be able to tweak a little bit our chat response:

// src/app/chat/src/lib/ui/navbar/navbar.tsx

import { Box, Button, Flex, Select, Slider, Stack, Text, Title, Tooltip, } from '@mantine/core';
import { IconInfoCircle, IconMessage } from '@tabler/icons-react';

import { ChatState } from '@nx-chat/openai.api';

/* eslint-disable-next-line */
export interface NavbarProps {
btnDisabled: boolean;
handleChatReset: () => void;
chatState: ChatState;
updateChatState: (newState: ChatState) => void;
}

export function Navbar({
btnDisabled,
handleChatReset,
chatState,
updateChatState,
}: NavbarProps) {
const parameters = [ { name: 'Model', value: chatState.model, options: ['gpt-3.5-turbo', 'gpt-4'],
key: 'model',
},
{
name: 'Temperature',
value: chatState.temperature,
min: 0,
max: 1,
key: 'temperature',
label: 'Controls the randomness of the model’s output.',
},
{
name: 'Top P',
value: chatState.topP,
min: 0,
max: 1,
key: 'topP',
label: 'Set probability threshold for more relevant outputs.',
},
{
name: 'Max Tokens',
value: chatState.maxTokens,
key: 'maxTokens',
label:
'The maximum number of tokens the model will generate in a single response.',
},
{
name: 'Frequency Penalty',
value: chatState.frequencyPenalty,
min: 0,
max: 1,
key: 'frequencyPenalty',
label:
'Adjusts the likelihood of the model repeating words or phrases in its output.',
},
{
name: 'Presence Penalty',
value: chatState.presencePenalty,
min: 0,
max: 1,
key: 'presencePenalty',
label:
'Influences the generation of new and varied concepts in the model’s output.',
},
];

return (
<Stack justify="space-between" h="100vh">
<Box p={20}>
<Title order={4}>Adjust parameters</Title>
<Stack>
{parameters.map((param, index) => (
<div key={index}>
<Flex align="center" gap="md">
<Text size="sm" mt="sm">
{param.name}
</Text>
{param.label && (
<Tooltip label={param.label}>
<IconInfoCircle size={20} />
</Tooltip>
)}
</Flex>
{param.options ? (
<Select
data={param.options.map((option) => ({
value: option,
label: option,
}))}
value={param.value}
onChange={(value) =>
value && updateChatState({ [param.key]: value })
}
mt={10}
/>
) : (
<Slider
value={param.value}
onChange={(value) => updateChatState({ [param.key]: value })}
defaultValue={param.value}
min={param.min}
max={param.max}
step={param.key === 'maxTokens' ? 1 : 0.01}
labelTransitionProps={{
transition: 'skew-down',
duration: 150,
timingFunction: 'linear',
}}
/>
)}
</div>
))}
</Stack>
</Box>
<Box p={20}>
<Button
size="xl"
rightSection={<IconMessage size={20} />}
onClick={handleChatReset}
disabled={btnDisabled}
>
New conversation
</Button>
</Box>
</Stack>
);
}

export default Navbar;

Footer

The footer component includes a textarea where we will be typing our message:

// src/app/chat/src/lib/ui/footer/footer.tsx

import { ActionIcon, Textarea } from '@mantine/core';
import { IconSend2 } from '@tabler/icons-react';

/* eslint-disable-next-line */
export interface FooterProps {
chatMessageText: string;
handleChatSend: () => void;
updateChatMessageText: (chatMessageText: string) => void;
}

export function Footer({
chatMessageText,
updateChatMessageText,
handleChatSend,
}: FooterProps) {
return (
<Textarea
size="xl"
value={chatMessageText}
onChange={(event) => updateChatMessageText(event.currentTarget.value)}
placeholder="Send a message"
rightSection={
chatMessageText && (
<ActionIcon
onClick={handleChatSend}
aria-label="Message"
variant="transparent"
size="input-lg"
>
<IconSend2 />
</ActionIcon>
)
}
/>
);
}

export default Footer;

Main

The main component is where chat responses will appear:

// src/app/chat/src/lib/ui/main/main.tsx

import { Card, Center, Grid, Loader, Paper, Stack, Text, ThemeIcon } from '@mantine/core';
import { IconRobot, IconUser } from '@tabler/icons-react';

/* eslint-disable-next-line */
export interface MainProps {
message: { text: string; isBot: boolean; }[];
btnDisabled: boolean;
targetRef: React.RefObject<HTMLDivElement>;
}

export function Main({ message, btnDisabled, targetRef }: MainProps) {
return (
<Stack justify="space-between">
{message.map((m, i) => (
<Card
key={i}
shadow="sm"
bg={m.isBot ? '' : 'transparent'}
padding="lg"
radius="md"
withBorder
styles={{
root: {
backgroundColor: m.isBot ? '' : 'transparent',
borderColor: m.isBot ? '' : 'transparent',
boxShadow: m.isBot ? '' : 'none',
},
}}
>
<Grid>
<Grid.Col span={1}>
<Center>
<ThemeIcon
variant="gradient"
size="xl"
aria-label="Gradient action icon"
gradient={{ from: m.isBot ? 'blue' : 'green', to: m.isBot ? 'indigo' : 'teal', deg: 120 }}
>
{m.isBot ? <IconRobot /> : <IconUser />}
</ThemeIcon>
</Center>
</Grid.Col>
<Grid.Col span={11}>
<Text size="md" c="white">
{m.text}
</Text>
</Grid.Col>
</Grid>
</Card>
))}
{btnDisabled && (
<Center>
<Loader color="blue" type="dots" />
</Center>
)}
<Paper className="gfreg" ref={targetRef} />
</Stack>
);
}

export default Main;

Final layout

Nx Mantine OpenAI Chat

Summary

The ability to create a simple yet modern applications quickly with minimal setup that Nx offers is one of the biggest benefits of using it. I was able to build a small application very fast which can be expanded in the future.

I showed you how to create workspace and how to create libraries and components. These were only basic futures that Nx supports. If you like it, I strongly suggest to deep-dive to more advanced options that Nx offers.

ps. Nx Chat

Very useful tool that helps you working with Nx is Nx… chat :) Yes, Nx has its own chatbot that can help you work with Nx apps where you can ask for anything related with Nx.

We’re always looking for new talent! View jobs.

Follow us: Facebook | Twitter | LinkedIn | Instagram

--

--