UI ‐ client side - fidransky/kiv-pia-labs GitHub Wiki

TOTD:

  • touch on differences between server-side and client-side rendered user interfaces (UIs)
  • learn about component-based UIs
  • implement the base of a client-side rendered UI for the semester project app

Terms and theory

All user interfaces on the web are running on the same programming languages triplet:

  1. HTML
  2. CSS
  3. JavaScript

However, not all UIs are composed together (rendered) the same way.

Server-side vs. client-side rendering

Historically, websites used to be mostly rendered on the server-side. With the recent increase of JavaScript capabilities and browser incompatibilities fading away, client-side rendering became a feasible option. How exactly are these two approaches different?

Server-side rendering

Using server-side rendering approach means that all HTML is composed together on the server and only then sent over the wire to the browser. All the browser has to do is to actually render the provided HTML, without any heavy-lifting. As you may have guessed, it results in several benefits:

  • performance - browser doesn't have to bother with HTML composition using loads of JavaScript so it works great even on low-end mobile devices
  • speed - rendering HTML right away is always faster running JavaScript to compose the HTML and then rendering it
  • SEO - search engines and other web crawlers don't need to run any JavaScript to fetch page contents

Admittedly, there are some downsides as well:

  • interactivity - page navigation as well as forms submission means running the whole roundtrip to server and back
  • data transmission - even though individual UI screens share some common parts (header, navigation, ...), these parts are transmitted again and again with each navigation

Client-side rendering

Even with client-side rendering, a small HTML skeleton is still provided by the server. However, most of the rendering is actually done by JavaScript. As a result, the browser has to download, parse and execute the JavaScript just to compose the HTML. Only then, the browser can finally render the UI. Ups and downs of this approach are opposite to the server-side rendering.

Since there's only one HTML page downloaded from the server, client-side rendered apps are also called SPAs (single-page applications).

Clearly, when JavaScript is not available, nothing else but the small HTML skeleton is rendered. The JavaScript not available situation happens more frequently than you might think. Except for the obvious case when users have JS disabled, there are other occurrences:

  • JavaScript failed to download
  • JavaScript hasn't finished downloading yet, resulting in a flash of blank screen
  • browser extension went awry and broke application JavaScript

Client-side rendering can be implemented using two appoaches:

  1. manual DOM manipulation
  2. state-based UI (Angular, Vue.js, React, …)

What to use

  • For websites and apps consisting of only a few screens, go with server-side rendering.
  • For complex and massively interactive web apps, go with client-side rendering.

In practice, a combination of both approaches is often used.

AJAX and Fetch API

Since SPAs run in a browser, they don't have a direct access to a database (or any other server store). As a result, they often use WS APIs to load the necessary data.

Historically, data loading has been done using AJAX (Asynchronous JavaScript and XML). While AJAX is still a viable option, we now have an alternative in Fetch API.

Router

Application router is a component which accepts a URL and decides which page should be rendered. Router is typically called upon page navigation, triggered either by clicking a link or submitting a form.

In server-side rendered apps, browser does all the heavy lifting for us. In client-side apps, we have to recreate the behavior.

Practice

In today's lab, we're going to create a simple UI for the app using client-side rendering. To do that, we're going to use Vite to build a plain React app in Typescript.

Warning

This section is not fully updated for the 2024/2025 course.

1. Create React app

Use Vite to generate a React app skeleton. Execute the following command in the root directory of your pia-labs project:

npm create vite@latest pia-labs-react-ui -- --template react-ts

Then, as instructed, go to the generated directory, install dependencies and start the React app:

cd pia-labs-react-ui
npm install
npm run dev

Open http://localhost:5173 in your browser to see what Vite generated for you.

Finally, try to create a production build of your app and see what gets created in the build/ folder:

npm run build

2. Generate Typescript client

We're going to use the previously implemented WS API to load data into our React app. Again, we're going to use API specs to generate some code - in this case, in Typescript.

2.1 Generate REST API client

Add yet another submodule to your pia-labs Maven project called pia-labs-typescript-client.

Add OpenAPI generator plugin to the build > plugins section, copying the configuration from Github:

<plugin>
	<groupId>org.openapitools</groupId>
	<artifactId>openapi-generator-maven-plugin</artifactId>
	<version>7.8.0</version>
	<configuration>
		<!-- copy the configuration from GitHub -->
	</configuration>
	<executions>
		<execution>
			<phase>generate-sources</phase>
			<goals>
				<goal>generate</goal>
			</goals>
		</execution>
	</executions>
</plugin>

Run mvn compile in pia-labs-typescript-client submodule to execute the plugin.

Then, open target/generated-sources/openapi/src/ directory to see what it generated.

2.2 Build the generated client

The plugin generates source code as a Node.js package written in Typescript. We're going to add the package as a dependency of our pia-labs-react-ui React app. However, before doing so, we must compile the generated code from Typescript to JavaScript.

We're going to use another Maven plugin to execute two npm commands:

  1. npm install - installs dependencies of the generated package
  2. npm run build - compiles the package using Typescript compiler

Add org.codehaus.mojo:exec-maven-plugin to the build > plugins section, copying the executions from Github:

<plugin>
	<groupId>org.codehaus.mojo</groupId>
	<artifactId>exec-maven-plugin</artifactId>
	<version>3.4.1</version>
	<configuration>
		<executable>npm</executable>
		<workingDirectory>${project.build.directory}/generated-sources/openapi/</workingDirectory>
	</configuration>
	<executions>
		<!-- copy the executions from GitHub -->
	</executions>
</plugin>

2.3 Add the generated client as a dependency

Finally, add the generated and compiled REST API client as a dependency of your React app:

npm install ..\pia-labs-typescript-client\target\generated-sources\openapi\

Now, you can create an instance of the generated DamagesApi class and use it to load some data from the REST API:

import { Configuration, DamageApi } from 'pia-labs-typescript-client';

// ...

const basePath = 'http://localhost:8080';
const configuration = new Configuration({ basePath });
const damageApi = new DamageApi(configuration);

// ...

damageApi.retrieveDamage()
	.then((data) => console.log(data));

3. Configure CORS

CORS (Cross Origin Resource Sharing) is a browser feature preventing client-side apps running on a different domain (therefore cross-origin) from accessing server resources. By default, browsers block our client-side app running at localhost:5173 from accessing data using WS API running at localhost:8080.

Configure the Spring app to allow CORS for WS API from localhost:5173 origin by extending the BeanConfiguration configuration class:

@Bean
public WebMvcConfigurer corsConfigurer() {
	return new WebMvcConfigurer() {
		@Override
		public void addCorsMappings(@NonNull CorsRegistry registry) {
			registry.addMapping("/**").allowedOrigins("http://localhost:5173");
		}
	};
}

4. Add router

There are many libraries implementing application router as a React component. One of them, usable in both web and native apps, is React Router.

Install React Router dependency:

npm install react-router-dom

Create a new React function component called Router in Router.tsx file:

import { createBrowserRouter, RouterProvider } from 'react-router-dom'
import App from './App'

const router = createBrowserRouter([
	{
		path: '/',
		element: <App/>,
	},
])

export default function Router() {
	return (
		<RouterProvider router={router}/>
	)
}

Use the newly created Router function component in main.js instead of the App:

// ...

root.render(
	<React.StrictMode>
		<Router/>
	</React.StrictMode>
);

Now, when accessing http://localhost:5173, React Router checks the URL first and matches it to correct route - in our case, the URL path is / (root) and so App element is rendered.

5. Implement base layout and use it for index page listing reported damage

base layout:

Implement base layout in index.html and App.tsx files so that:

  • non-interactive markup (such as basic layout and footer) goes to index.html
  • interactive components (such as header) goes to App.tsx

Optionally, split the common header from App.tsx to a standalone Header function component and use it in App.

Optionally, add Bootstrap to make styling easier.

reported damage listing:

Next, create a new Home function component in Home.tsx file:

function Home() {
	// TODO: render a list of reported damage
	return 'implement me'
}

export default Home

With the Home component created, add another route to the Router component as a child of the previously created root route:

{
	path: '/',
	element: <App />,
	children: [
		{
			index: true,
			element: <Home />,
		},
	],
},

Back to the Home component:

  1. Use useState React hook to create damages component state
  2. Use useEffect React hook to load a list of reported damage on component mount
  3. Store the loaded reported damage list to the component state
const [ damages, setDamages ] = React.useState<DamageDTO[]>([])

// ...

React.useEffect(() => {
	damageApi.retrieveDamage()
		.then((damages) => {
				setDamages(damages)
		})
}, [])

Finally, render the reported damage list stored in the component state in a list:

return <ul>
	{damages.map((damage) => {
		return (
			<li key={damage.id}>
				{damage.description}
			</li>
		)
	})}
</ul>

6. Split WS API calls into a service

In server-side apps, it's not advised to call repositories directly from controllers in the sake of layer separation. Instead, we created a service layer and domain objects sitting and used it to transfer data from controllers to repositories and back.

Same principle applies to client-side apps too. We shouldn't call methods of the generated DamageApi class directly from components but rather:

  1. define domain objects as Typescript types in types.ts file
  2. create a service layer calling Api methods and mapping data transfer objects (DTOs) to domain objects in DamageService.ts file

Then, we use the newly created DamageService in our Home function component.

7. Generate GraphQL client (optional)

Instead of REST API, we may as well use the previously implemented GraphQL API in the React app. To generate Typescript types from the GraphQL schema, use GraphQL Code Generator.

Install GraphQL codegen as the React app's development dependency:

npm install -D @graphql-codegen/cli @graphql-codegen/typescript

Create GraphQL codegen's configuration in codegen.ts file:

import type { CodegenConfig } from '@graphql-codegen/cli'

const config: CodegenConfig = {
	schema: '../pia-labs-ws/src/main/resources/graphql/schema.graphqls',
	generates: {
		'./src/generated-types.ts': {
			plugins: ['typescript'],
		},
	},
}
export default config

Use GraphQL codegen to generate Typescript types to generated-types.ts file:

graphql-codegen

Implement GraphQLClient and GraphQLMappers.

Use GraphQLClient in DamageService to load reported damage using GraphQL API instead of REST API. No changes in other parts of the app are needed thanks to proper code layer separation.

Sources

⚠️ **GitHub.com Fallback** ⚠️