DEV Community

alexia cismaru
alexia cismaru

Posted on

How to build a website using React and Rest APIs (React basics explained)

Image description

React and TypeScript are powerful frameworks for building scalable, maintainable, and safe websites. React provides a flexible and component-based architecture, while TypeScript adds static typing to JavaScript, for clean and readable code. This article will guide you through setting up a simple website with React and TypeScript, covering the core concepts needed to get started.

Why Choose React with TypeScript?

TypeScript is popular among JavaScript developers because it can catch errors during development and make code easier to understand and refactor. The two are ideal for building modern, fast websites and applications with maintainable code that scales well.

** Check out the whole code on GitHub: https://github.com/alexiacismaru/techtopia/tree/main/frontend

Basic React concepts and how to use them to build a website

Let’s build a website for a fictional amusement park called Techtopia. We will show elements like attractions and where they are on the map, a landing page, or a loading page. In addition, we will also make it possible to add/delete elements of the page or to search for them based on a variable.

Setup

Create an empty React project by copying this into the terminal.

npm create vite@latest reactproject --template react-ts
Enter fullscreen mode Exit fullscreen mode

Then run the empty project and a new tab will open in the browser window.

cd reactproject
npm run dev
Enter fullscreen mode Exit fullscreen mode

Final project structure overview

reactproject/
├── node_modules/
├── public/
├── src/
│   ├── assets/
│   ├── components/
│   ├── context/
│   ├── hooks/
│   ├── model/
│   ├── services/
│   ├── App.css
│   ├── App.tsx
│   ├── index.css
│   ├── vite-env.d.ts
├── .gitignore
├── package.json
└── tsconfig.json
Enter fullscreen mode Exit fullscreen mode

Components

Components are elements of a webpage that can also be reused. They can be a part of the webpage, like the header or footer, or the entire page, like a list of users. It is just like a JavaScript function but returns a rendered element.

export function Header() {
    return (
        <header style={{ display: 'block', width: '100%', top: 0, left: 0, zIndex: 'var(--header-and-footer)' }}>
            <div style={{
                borderBottom: '1px solid white',
                boxShadow: '',
                backgroundColor: 'transparent',
                paddingLeft: '1rem',
                paddingRight: '1rem',
                marginLeft: 'auto',
                marginRight: 'auto',
            }}>
                <div
                    style={{
                        display: 'flex',
                        justifyContent: 'space-between',
                        padding: '5px',
                        alignItems: 'baseline',
                    }}
                >
                    <a href='/techtopia' style={{
                        fontSize: '40px', fontFamily: 'MAROLLA__', color: 'black',
                        fontWeight: 'bold',
                    }}>Techtopia</a>
                    <div style={{display: 'flex',
                        justifyContent: 'space-around',
                        padding: '5px',
                        alignItems: 'baseline',}}>
                        <a href='/refreshment-stands' style={{
                            marginRight: '10px', color: 'black'
                        }}>Refreshment stands</a>
                        <a href='/attractions' style={{ marginRight: '10px', color: 'white'
                        }}>Attractions</a>
                        <a href='/map' style={{ marginRight: '60px', color: 'white'
                        }}>Map</a>
                    </div>
                </div>
            </div>
        </header>
    )
}
Enter fullscreen mode Exit fullscreen mode

JSX

JSX is JavaScript XML, allowing the user to write HTML-like code in .jsx files.

<Button sx={{padding: "10px", color: 'black'}} onClick={onClose}>X</Button>
Enter fullscreen mode Exit fullscreen mode

TSX

TSX is a file extension for TypeScript files that contains JSX syntax. With TSX you can write type-checked code with the existing JSX syntax.

interface RefreshmentStand {
    id: string;
    name: string;
    isOpen: boolean;
}

const Reshfresment = (props: RefreshmentStand) => {
  return (
    <div>
        <h1>{props.name}</h1>
        <p>{props.isOpen}</p>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Fragments

Fragments return multiple elements to a component. It groups the list of elements without creating extra DOM nodes.

We can use them to fetch the data from a Java backend (check out how to build the Java application from this article: https://medium.com/@alexia.csmr/using-bounded-contexts-to-build-a-java-application-1c7995038d30). Start by installing Axios and using the base backend URL from your application. Then, we will create a fragment that uses GET to fetch all the attractions.

import axios from 'axios'
import { POI } from '../model/POI'

const BACKEND_URL = 'http://localhost:8093/api'

export const getAttractions = async () => {
    const url = BACKEND_URL + '/attractions'
    const response = await axios.get<POI[]>(url)
    return response.data
}
Enter fullscreen mode Exit fullscreen mode

This can be expanded to getting data based on parameters, POST, DELETE, etc.

export const addAttraction = async (attractionData: Omit<POI, 'id'>) => {
    const url = BACKEND_URL + '/addAttraction'
    const response = await axios.post(url, attractionData)
    return response.data
}

export const getAttraction = async (attractionId: string) => {
    const url = BACKEND_URL + '/attractions'
    const response = await axios.get<POI>(`${url}/${attractionId}`)
    return response.data
}

export const getAttractionByTags = async (tags: string) => {
    const url = BACKEND_URL + '/attractions'
    const response = await axios.get<POI[]>(`${url}/tags/${tags}`)
    return response.data
}
Enter fullscreen mode Exit fullscreen mode

State

The state is a React object that contains data or information about the component. A component’s state can change over time and when it does, the component re-renders.

To get a single element from a list based on a parameter you can use the useParams() hook.

const { id } = useParams()
const { isLoading, isError, attraction } = useAttraction(id!)
const { tag } = useParams()
const { isLoadingTag, isErrorTag, attractions } = useTagsAttractions(tag!)
Enter fullscreen mode Exit fullscreen mode

Hooks

As seen above, I’ve used_ useAttractions() and _useTagsAttractions(). They are hooks and can be personalized to get any data you want. In this example, they fetch the attractions based on their ID _or _tags. Hooks can only be called inside React function components, can only be called at the top level of a component, and can’t be conditional.

import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {POI} from "../model/./POI.ts";
import { addAttraction, getAttractions } from '../services/API.ts'
import { useContext } from 'react'

export function useAttractions() {
    const queryClient = useQueryClient()
    const {
        isLoading: isDoingGet,
        isError: isErrorGet,
        data: attractions,
    } = useQuery({
        queryKey: ['attractions'],
        queryFn: () => getAttractions(),
    })

    const {
        mutate,
        isLoading: isDoingPost,
        isError: isErrorPost,
    } = useMutation((item: Omit<POI, 'id'>) => addAttraction(item), {
        onSuccess: () => {
            queryClient.invalidateQueries(['attractions'])
        },
    });

    return {
        isLoading: isDoingGet || isDoingPost,
        isError: isErrorGet || isErrorPost,
        attractions: attractions || [],
        addAttraction: mutate
    }
}
Enter fullscreen mode Exit fullscreen mode

isLoading and isError

For a better UI experience, it’s good to let the user know what’s happening i.e. the elements are loading, or there has been an error when doing so. They are first declared in the hook and then introduced in the component.

const navigate = useNavigate()
const { isLoading, isError, attractions, addAttraction } = useAttractions()

if (isLoading) {
    return <Loader />
}

if (isError) {
    return <Alert severity='error'>Error</Alert>
}
Enter fullscreen mode Exit fullscreen mode

You can also create a separate Loader or Alert component for a more customized website.

export default function Loader() {
    return (
        <div>
            <img alt="loading..."
                 src="https://media0.giphy.com/media/RlqidJHbeL1sPMDlhZ/giphy.gif?cid=6c09b9522vr2magrjgn620u5mfz1ymnqhpvg558dv13sd0g8&ep=v1_stickers_related&rid=giphy.gif&ct=s"/>
            <h3>Loading...</h3>
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Now, when the page is loading the user will see a special animation on the screen.

Mapping items (Lists and Keys)

If you want to display all the elements in a list then you need to map through all of them.

import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useAttractions } from '../hooks/usePOI.ts'
import { POI } from '../model/./POI.ts'

export default function Attractions() {
    const navigate = useNavigate()
    const { isLoading, isError, attractions, addAttraction } = useAttractions()

    return (
      <div style={{ marginTop: '70px' }}>
          {filteredAttractions
              .map(({ id, name, image }: POI) => (
                  <div onClick={() => navigate(`/attractions/${id}`)} >
                      <div>
                          <img src={image} alt={name}/>
                          <h3>{name}</h3>
                      </div>
                  </div>
              ))}
      </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Create a separate file where you declare the Attraction element and its variables.

// ../model/POI.ts

export interface POI {
    id: string;
    name: string;
    description: string;
    tags: string;
    ageGroup: string;
    image: string;
}
Enter fullscreen mode Exit fullscreen mode

More here you can create a type to later add more attractions using a form:

export type CreatePOI = Omit<POI, 'id'>; # id is automatically generated so we don't need to manually add it
Enter fullscreen mode Exit fullscreen mode

Adding items

We already created the fragments and hooks needed for this, so now we can make a form where the user can write the attributes and add a new attraction to the webpage. This form was created using the MUIframework. First I’ll show the whole code and explain it in sections.

import {CreatePOI} from "../model/./POI.ts";
import {z} from 'zod';
import {zodResolver} from "@hookform/resolvers/zod";
import {Controller, useForm} from "react-hook-form";
import {
    Box,
    Button,
    Dialog,
    DialogActions,
    DialogContent,
    DialogTitle,
    TextField,
} from '@mui/material'

interface AttractionDialogProps {
    isOpen: boolean;
    onSubmit: (attraction: CreatePOI) => void;
    onClose: () => void;
}

const itemSchema: z.ZodType<CreatePOI> = z.object({
    name: z.string().min(2, 'Name must be at least 2 characters'),
    description: z.string(),
    tags: z.string(),
    ageGroup: z.string(),
    image: z.string().url(),
})

export function AddAttractionDialog({isOpen, onSubmit, onClose}: AttractionDialogProps) {
    const {
        handleSubmit,
        control,
        formState: {errors},
    } = useForm<CreatePOI>({
        resolver: zodResolver(itemSchema),
        defaultValues: {
            name: '',
            description: '',
            tags: '',
            ageGroup: '',
            image: '',
        },
    });

    return (
        <Dialog open={isOpen} onClose={onClose}>
            <form
                onSubmit={handleSubmit((data) => {
                    onSubmit(data)
                    onClose()
                })} 
            >
                <div>
                    <DialogTitle>Add attraction</DialogTitle>
                    <Button onClick={onClose}>
                        X
                    </Button>
                </div>
                <DialogContent>
                    <Box>
                        <Controller
                            name="name"
                            control={control}
                            render={({field}) => (
                                <TextField
                                    {...field}
                                    label="Name"
                                    error={!!errors.name}
                                    helperText={errors.name?.message}
                                    required
                                />
                            )}
                        />
                        <Controller
                            name="description"
                            control={control}
                            render={({field}) => (
                                <TextField
                                    {...field}
                                    label="Description"
                                    error={!!errors.description}
                                    helperText={errors.description?.message}
                                />
                            )}
                        />
                        <Controller
                            name="tags"
                            control={control}
                            render={({field}) => (
                                <TextField
                                    {...field}
                                    label="Tags"
                                    error={!!errors.tags}
                                    helperText={errors.tags?.message}
                                    required
                                />
                            )}
                        />
                        <Controller
                            name="ageGroup"
                            control={control}
                            render={({field}) => (
                                <TextField
                                    {...field}
                                    label="Age group"
                                    error={!!errors.ageGroup}
                                    helperText={errors.ageGroup?.message}
                                    required
                                />
                            )}
                        />
                        <Controller
                            name="image"
                            control={control}
                            render={({field}) => (
                                <TextField
                                    {...field}
                                    label="Image"
                                    error={!!errors.image}
                                    helperText={errors.image?.message}
                                    required
                                />
                            )}
                        />
                    </Box>
                </DialogContent>
                <DialogActions>
                    <Button type="submit" variant="contained">
                        Add
                    </Button>
                </DialogActions>
            </form>
        </Dialog>
    )
}
Enter fullscreen mode Exit fullscreen mode

If you want to make the form a pop-up instead of a separate page, add isOpen() and isClosed() attributes. onSubmit() is mandatory since this will trigger the createPOI() function and add a new object to the list.

interface AttractionDialogProps {
    isOpen: boolean;
    onSubmit: (attraction: CreatePOI) => void;
    onClose: () => void;
}
Enter fullscreen mode Exit fullscreen mode

For user form validation we will install and import Zod. Here declare what format the input needs to be and if there are any requirements like minimum or maximum length.

const itemSchema: z.ZodType<CreatePOI> = z.object({
    name: z.string().min(2, 'Name must be at least 2 characters'),
    description: z.string(),
    tags: z.string(),
    ageGroup: z.string(),
    image: z.string().url(),
})
Enter fullscreen mode Exit fullscreen mode

Inside the component, we need to implement the submit and the user validation.

const {
    handleSubmit,
    control,
    formState: {errors},
    } = useForm<CreatePOI>({
    resolver: zodResolver(itemSchema),
    defaultValues: {
        name: '',
        description: '',
        tags: '',
        ageGroup: '',
        image: '',
    },
});
Enter fullscreen mode Exit fullscreen mode

The errors will be implemented in the TextField of the form with any other attributes.

<TextField
    {...field}
    label="Name"
    error={!!errors.name}
    helperText={errors.name?.message}
    required
/>
Enter fullscreen mode Exit fullscreen mode

Make sure that the form can be closed and submitted in the beginning.

<Dialog open={isOpen} onClose={onClose}>
    <form
        onSubmit={handleSubmit((data) => {
            onSubmit(data)
            onClose()
        })} 
    >     
    </form>
</Dialog>
Enter fullscreen mode Exit fullscreen mode

You can implement this pop-up in another component.

import { Fab } from '@mui/material'
import AddIcon from '@mui/icons-material/Add'

<Fab
    size='large'
    aria-label='add' 
    onClick={() => setIsDialogOpen(true)}
>
    <AddIcon />
</Fab>
Enter fullscreen mode Exit fullscreen mode

Deleting items

Create a hook that uses DELETE and implement it in a component.

import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { deleteRefreshmentStand, getRefreshmentStand } from '../services/API.ts'
import { useContext } from 'react' 

export function useRefreshmentStandItem(refreshmentStandId: string) { 
    const queryClient = useQueryClient()

    const {
        isLoading: isDoingGet,
        isError: isErrorGet,
        data: refreshmentStand,
    } = useQuery({
        queryKey: ['refreshmentStand'],
        queryFn: () => getRefreshmentStand(refreshmentStandId), 
    })

    const deleteRefreshmentStandMutation = useMutation(() => deleteRefreshmentStand(refreshmentStandId), {
        onSuccess: () => {
            queryClient.invalidateQueries(['refreshmentStands']);
        },
    });

    const handleDeleteRefreshmentStand = () => {
        deleteRefreshmentStandMutation.mutate(); // Trigger the delete mutation
    };

    return {
        isLoading: isDoingGet || deleteRefreshmentStandMutation.isLoading,
        isError: isErrorGet || deleteRefreshmentStandMutation.isError,
        refreshmentStand,
        deleteRefreshmentStand: handleDeleteRefreshmentStand,
    };
}
Enter fullscreen mode Exit fullscreen mode
export default function RefreshmentStand() {
    const { id } = useParams()
    const { isLoading, isError, refreshmentStand, deleteRefreshmentStand } = useRefreshmentStandItem(id!)


    if (isLoading) {
        return <Loader />
    }

    if (isError || !refreshmentStand) {
        return <Alert severity='error'>Error</Alert>
    }

    return (
        <>
            <CardMedia component='img' image={background} alt='background' />
            <AuthHeader />
            <div style={{ display: 'flex', alignItems: 'center' }}>
                <div>
                    <h1>{refreshmentStand.name}</h1>
                    <p>Status: {refreshmentStand.isOpen ? 'Open' : 'Closed'}</p>
                    /* implement the delete button */
                    <Fab>
                        <DeleteIcon onClick={deleteRefreshmentStand}/>
                    </Fab>
                </div>
                <img src={refreshmentStand.image} alt='refreshmentStand image' />
            </div>
            <Footer />
        </>
    )
}
Enter fullscreen mode Exit fullscreen mode

Filtering items

Inside the component create a toggle for the filter text input and a constant that filters the attractions based on the age group or tags. Optional chaining (?) ensures it handles null or undefined values without errors.

const toggleFilter = () => {
    setIsFilterOpen(!isFilterOpen)
}

const filteredAttractions = attractions
    .filter((attraction: POI) =>
        attraction.ageGroup?.toLowerCase().includes(ageGroupFilter.toLowerCase()),
    )
    .filter((attraction: POI) =>
        attraction.tags?.toLowerCase().includes(tagsFilter.toLowerCase()),
    )
Enter fullscreen mode Exit fullscreen mode

Include it when iterating through the list of items.

{filteredAttractions
.filter((attraction: POI) =>
    searchText.trim() === '' ||
    attraction.name.toLowerCase().includes(searchText.toLowerCase()),
)
.map(({ id, name, image }: POI) => (
    <div>
        <div>
            <img
                src={image}
                alt={name}
            />
            <h3>{name}</h3>
        </div>
    </div>
))}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Using React with TypeScript enables you to build dynamic, safe websites that are easy to maintain and scale. TypeScript’s type-checking prevents runtime errors, while React’s component-based structure organizes the project efficiently.

Top comments (2)

Collapse
 
ragul_kl profile image
Ragul KL • Edited

I appreciate the thorough guide! I would recommend trying out Sparkout, which greatly facilitates the development of React websites.