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
All user interfaces on the web are running on the same programming languages triplet:
- HTML
- CSS
- JavaScript
However, not all UIs are composed together (rendered) the same way.
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?
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
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:
- manual DOM manipulation
- state-based UI (Angular, Vue.js, React, …)
- 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.
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.
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.
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.
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
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.
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.
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:
-
npm install
- installs dependencies of the generated package -
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>
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));
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");
}
};
}
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.
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:
- Use
useState
React hook to createdamages
component state - Use
useEffect
React hook to load a list of reported damage on component mount - 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>
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:
- define domain objects as Typescript types in
types.ts
file - create a service layer calling
Api
methods and mapping data transfer objects (DTOs) to domain objects inDamageService.ts
file
Then, we use the newly created DamageService
in our Home
function component.
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.