This content originally appeared on DEV Community and was authored by u4aew
Introduction
Hello! In this article, we will explore how to create a simple web application with server-side rendering (SSR) and why it might be useful.
Server-side rendering (SSR) allows us to deliver pages to users more quickly, improving performance metrics and user experience. This is especially beneficial when users have budget smartphones or slow internet connections, as all heavy requests and computations are handled on the server.
For SSR, we will use Next.js 14. Next.js is a popular framework that offers many useful features out of the box:
- Server-side and static rendering
- Automatic code optimization
- Built-in TypeScript support
- Smart routing
- API routes for building a backend
- Version 14+ introduces new capabilities, including enhanced performance, a new file-system-based router, and improved support for server components.
Project Idea
For our example, we will develop an MVP of a financial exchange with basic functionality. Here’s what our application will be able to do:
- Display a stock catalog with pagination
- Load additional stocks via a "Show More" button
- Provide detailed information about each stock on a separate page
- Show the latest price and percentage change for each stock
- Display a price change chart for each stock
- Be SEO-optimized
- Be server-rendered for improved performance
Implementation
In Next.js, there is a concept of server and client components. Let's explore their differences:
Server Components are processed on the server. They perform API requests, fetch data, and generate HTML markup, which the browser then renders. Advantages include:
- Reduced JavaScript sent to the client
- Faster initial page load
- Direct access to server resources (e.g., databases)
- Improved SEO optimization
Client Components function like regular React components in the browser. They can:
- Interact with browser APIs
- Handle user events
- Manage local state
- Use React lifecycle hooks
Here's how we will divide the components of our application:
Server Components (Red):
- Header: Static, does not require interactivity
- Stock List: Data is loaded on the server
- Pagination: Logic is handled on the server for SEO
- Stock Details: Data fetched from the server
Client Components (Green):
- "Load More" Button: Requires click handling and scroll position preservation
- Price Change Chart: Interactive, uses browser APIs for rendering
- This separation allows us to leverage the strengths of both server and client components effectively.
Working with the API
To handle stock market data, we will use a public API, but we'll create a BFF (Backend for Frontend) for it. The BFF will allow us to:
- Retrieve only the data necessary for our application
- Transform data into a format convenient for the frontend
- Combine data from multiple sources
- Cache frequently requested information
This approach will optimize data handling and improve performance.
Server Components
Here's an example of a server component: the stock information page.
import React from 'react';
import { Metadata } from 'next';
import { BASE_URL } from '@/config';
import { serviceStocks } from '@/services';
import { StockIntro } from '@/components/StockIntro';
import { BuyStock } from '@/components/BuyStock';
import Candles from '@/components/Candles/Candles';
import styles from './styles.module.scss';
type Props = {
params: { slug: string };
};
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { data: dataIntro } =
(await serviceStocks.getByTicker(params.slug)) || {};
if (!dataIntro) {
return {
title: 'Stock not found',
};
}
const metaDescription = `Information about stock ${dataIntro.name} (${dataIntro.ticker}). Sector: ${dataIntro.sector}, Country: ${dataIntro.countryOfRisk}`;
const keywords = `${dataIntro.name}, ${dataIntro.ticker}, ${dataIntro.sector}, ${dataIntro.countryOfRiskName}, stock, invest`;
const canonicalUrl = `${BASE_URL}/${params.slug}`;
return {
title: `${dataIntro.name} (${dataIntro.ticker}) - Information about stock`,
description: metaDescription,
keywords: keywords,
openGraph: {
title: `${dataIntro.name} (${dataIntro.ticker}) - Information about stock`,
description: metaDescription,
type: 'website',
},
alternates: {
canonical: canonicalUrl,
},
};
}
const PageStock = async ({ params }: Props) => {
try {
const [stockData, lastPriceData, candlesData] = await Promise.all([
serviceStocks.getByTicker(params.slug),
serviceStocks.getLastPriceByTicker(params.slug),
serviceStocks.getCandlesByTicker(params.slug),
]);
return (
<div className={styles.page}>
<div className={styles.main}>
<div className={styles.intro}>
{stockData ? (
<StockIntro data={stockData.data} />
) : (
<span>Not data</span>
)}
</div>
{candlesData ? (
<Candles
currency={stockData?.data?.currency}
data={candlesData.data}
/>
) : (
<span>Not data</span>
)}
</div>
<div className={styles.side}>
{lastPriceData && candlesData ? (
<BuyStock
candlesData={candlesData.data}
currency={stockData?.data?.currency}
data={lastPriceData.data}
/>
) : (
<span>Not data</span>
)}
</div>
</div>
);
} catch (error) {
console.error('Error loading stock data:', error);
return <div>Error loading stock data</div>;
}
};
export default PageStock;
Client Components
Here's an example of a client component for the price change chart:
'use client';
import React from 'react';
import dynamic from 'next/dynamic';
import styles from './styles.module.scss';
const Chart = dynamic(() => import('./Chart/Chart'), { ssr: false });
const Candles = ({ data, currency }: { data: any; currency?: string }) => {
return (
<div className={styles.chart}>
<Chart data={data} currency={currency} />
</div>
);
};
export default Candles;
import React, { FC } from 'react';
// @ts-ignore
import CanvasJSReact from '@canvasjs/react-charts';
import { ICandle } from '@/typing';
const CanvasJSChart = CanvasJSReact.CanvasJSChart;
interface ChartProps {
data: {
candles: ICandle[];
};
currency?: string;
}
const Chart: FC<ChartProps> = ({ data, currency }) => {
const dataPoints = data.candles.map((candle) => ({
x: new Date(candle.time),
y: [
parseFloat(`${candle.open.units}.${candle.open.nano}`),
parseFloat(`${candle.high.units}.${candle.high.nano}`),
parseFloat(`${candle.low.units}.${candle.low.nano}`),
parseFloat(`${candle.close.units}.${candle.close.nano}`),
],
}));
const options = {
theme: 'light2',
axisX: {
valueFormatString: 'DD MMM',
},
axisY: {
prefix: currency,
},
data: [
{
type: 'candlestick',
yValueFormatString: `${currency} ###0.00`,
xValueFormatString: 'DD MMM YYYY',
dataPoints: dataPoints,
},
],
};
return <CanvasJSChart options={options} />;
};
export default Chart;
For a more detailed look at the code, you can visit the GitHub repository.
SEO Optimization
import { Open_Sans } from 'next/font/google';
import { BASE_URL } from '@/config';
import { Footer } from '@/components/Footer/Footer';
import { Header } from '@/components/Header';
import { Metadata } from 'next';
import type { Viewport } from 'next';
import styles from './layout.module.scss';
import './globals.css';
const roboto = Open_Sans({
weight: '400',
subsets: ['latin'],
});
const RootLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return (
<html lang="en" className={roboto.className}>
<body className={styles.layout}>
<header className={styles.header}>
<Header />
</header>
<main className={styles.main}>
<div className={styles.content}>{children}</div>
</main>
<footer className={styles.footer}>
<Footer />
</footer>
</body>
</html>
);
};
export default RootLayout;
export const viewport: Viewport = {
colorScheme: 'light dark',
themeColor: '#2196f3',
};
export const metadata: Metadata = {
title: {
default: 'Financial Exchange Platform',
template: '%s | Financial Exchange Platform',
},
description:
'A leading financial exchange platform for trading and investment.',
applicationName: 'Financial Exchange Platform',
authors: [
{
name: 'Financial Exchange Team',
url: BASE_URL,
},
],
metadataBase: new URL(BASE_URL),
generator: 'Next.js',
keywords: ['financial', 'exchange', 'trading', 'investment', 'platform'],
referrer: 'origin',
creator: 'Financial Exchange Team',
publisher: 'Financial Exchange Inc.',
robots: { index: true, follow: true },
alternates: {
canonical: BASE_URL,
},
icons: {
icon: '/favicon.ico',
apple: '/apple-touch-icon.png',
},
manifest: '/manifest.webmanifest',
openGraph: {
title: 'Financial Exchange Platform',
description:
'A leading financial exchange platform for trading and investment.',
type: 'website',
url: BASE_URL,
siteName: 'Financial Exchange Platform',
images: [
{
url: '/images/og-image.jpg',
width: 800,
height: 600,
alt: 'Financial Exchange Platform',
},
],
},
appleWebApp: {
capable: true,
title: 'Financial Exchange Platform',
statusBarStyle: 'black-translucent',
},
formatDetection: {
telephone: false,
},
abstract: 'A leading financial exchange platform for trading and investment.',
category: 'Finance',
classification: 'Financial Services',
other: {
'msapplication-TileColor': '#0a74da',
},
};
Application Deployment
To automate the deployment of our application, we will use GitHub Actions and Docker Hub. This setup will allow us to automatically build and publish Docker images with each push to the main branch of the repository.
name: Docker
on:
push:
branches: [ main ]
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push Docker image
uses: docker/build-push-action@v2
with:
push: true
tags: u4aew/next:latest
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 6060
CMD ["npm", "start"]
The minimal basic functionality of our application is ready.
In the future, to enhance functionality, we will add:
- WebSocket for real-time price updates
- Filters and sorting for the stock catalog
- User authentication
- A personal dashboard to track selected stocks
Thank you for your attention, and I hope this article was helpful!
This content originally appeared on DEV Community and was authored by u4aew
u4aew | Sciencx (2024-09-06T18:48:51+00:00) Creating an SSR Application on Next.js 14. Retrieved from https://www.scien.cx/2024/09/06/creating-an-ssr-application-on-next-js-14/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.