A brief introduction to the concept of a Nx React standalone application
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:
- Clear architectural guidelines and best practices. Nx helps define efficient architectural guidelines for a maintainable, scalable React application.
- Adaptability: Nx enables you to start small with a single-project setup and grows it into a monorepo as needed.
- 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:
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.
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.
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).
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
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”.
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.
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.
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
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.
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.
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
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.