Xây dựng website với React và Rest API: Hướng dẫn cơ bản

0 0 0

Người đăng: Vinh Phạm

Theo Viblo Asia

React và TypeScript là những framework mạnh mẽ để xây dựng các website có khả năng mở rộng, dễ bảo trì và an toàn. React cung cấp một kiến trúc linh hoạt dựa trên thành phần, trong khi TypeScript bổ sung tính năng nhập liệu tĩnh cho JavaScript, giúp code sạch sẽ và dễ đọc hơn. Bài viết này sẽ hướng dẫn bạn thiết lập một website đơn giản với React và TypeScript, bao gồm các khái niệm cốt lõi cần thiết để bắt đầu.

Tại sao nên chọn React kết hợp với TypeScript?

TypeScript rất phổ biến trong cộng đồng các nhà phát triển JavaScript vì nó có thể phát hiện lỗi trong quá trình phát triển và làm cho code dễ hiểu và dễ tái cấu trúc hơn. Sự kết hợp của hai công nghệ này là lý tưởng để xây dựng các website và ứng dụng hiện đại, nhanh chóng với code dễ bảo trì và khả năng mở rộng tốt.

Các khái niệm cơ bản của React và cách sử dụng chúng để xây dựng website

Chúng ta sẽ xây dựng một website cho một công viên giải trí giả tưởng có tên là Techtopia. Website sẽ hiển thị các yếu tố như điểm tham quan và vị trí của chúng trên bản đồ, trang đích hoặc trang tải. Ngoài ra, chúng ta cũng sẽ làm cho nó có thể thêm/xóa các yếu tố của trang hoặc tìm kiếm chúng dựa trên một biến.

Thiết lập

Tạo một dự án React trống bằng cách sao chép đoạn code sau vào terminal:

npm create vite@latest reactproject --template react-ts

Sau đó, chạy dự án trống và một tab mới sẽ mở ra trong cửa sổ trình duyệt:

cd reactproject
npm run dev

Tổng quan cấu trúc dự án cuối cùng

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

Thành phần (Components)

Thành phần là các yếu tố của một trang web có thể được tái sử dụng. Chúng có thể là một phần của trang web, như tiêu đề hoặc chân trang, hoặc toàn bộ trang, như danh sách người dùng. Nó giống như một hàm JavaScript nhưng trả về một phần tử được hiển thị.

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> )
}

JSX

JSX là JavaScript XML, cho phép người dùng viết code giống HTML trong các file .jsx.

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

TSX

TSX là phần mở rộng tệp cho các tệp TypeScript chứa cú pháp JSX. Với TSX, bạn có thể viết code được kiểm tra kiểu với cú pháp JSX hiện có.

interface RefreshmentStand { id: string; name: string; isOpen: boolean;
} const Reshfresment = (props: RefreshmentStand) => { return ( <div> <h1>{props.name}</h1> <p>{props.isOpen}</p> </div> );
};

Fragments

Fragments trả về nhiều phần tử cho một thành phần. Nó nhóm danh sách các phần tử mà không tạo thêm các nút DOM.

Bắt đầu bằng cách cài đặt Axios và sử dụng URL backend cơ sở từ ứng dụng của bạn. Sau đó, chúng ta sẽ tạo một đoạn mã sử dụng GET để lấy tất cả các điểm tham quan.

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
}

Điều này có thể được mở rộng để lấy dữ liệu dựa trên các tham số, POST, DELETE, v.v.

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
}

State

State hay trạng thái là một đối tượng React chứa dữ liệu hoặc thông tin về thành phần. Trạng thái của thành phần có thể thay đổi theo thời gian và khi đó, thành phần sẽ được render lại.

Để lấy một phần tử duy nhất từ danh sách dựa trên tham số, bạn có thể sử dụng hook useParams().

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

Hook

Như đã thấy ở trên, tôi đã sử dụng _ useAttractions() và useTagsAttractions() . Chúng là các hook và có thể được cá nhân hóa để lấy bất kỳ dữ liệu nào bạn muốn. Trong ví dụ này, chúng lấy các điểm tham quan dựa trên ID or _tags của chúng . Hook chỉ có thể được gọi bên trong các thành phần hàm React, chỉ có thể được gọi ở cấp cao nhất của một thành phần và không thể có điều kiện.

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 }
}

isLoading và isError

Để có trải nghiệm UI tốt hơn, tốt nhất là cho người dùng biết những gì đang xảy ra, ví dụ như các thành phần đang tải hoặc đã xảy ra lỗi khi thực hiện. Đầu tiên, chúng được khai báo trong hook và sau đó được giới thiệu trong thành phần.

const navigate = useNavigate()
const { isLoading, isError, attractions, addAttraction } = useAttractions() if (isLoading) { return <Loader />
} if (isError) { return <Alert severity='error'>Error</Alert>
}

Bạn cũng có thể tạo thành phần Loader hoặc Alert riêng biệt để có trang web tùy chỉnh hơn.

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> )
}

Bây giờ, khi trang đang tải, người dùng sẽ thấy một hình ảnh động đặc biệt trên màn hình.

Ánh xạ các mục (Danh sách và Khóa)

Nếu bạn muốn hiển thị tất cả các phần tử trong danh sách thì bạn cần phải ánh xạ qua tất cả các phần tử đó.

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> )
}

Tạo một tệp riêng để khai báo phần tử Attraction và các biến của nó.

// ../model/POI.ts export interface POI { id: string; name: string; description: string; tags: string; ageGroup: string; image: string;
}

Tại đây bạn có thể tạo một loại để sau này thêm nhiều điểm tham quan hơn bằng cách sử dụng biểu mẫu:

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

Thêm mục

Chúng ta đã tạo các đoạn mã và hook cần thiết cho việc này, vì vậy bây giờ chúng ta có thể tạo một biểu mẫu nơi người dùng có thể viết các thuộc tính và thêm một điểm thu hút mới vào trang web. Biểu mẫu này được tạo bằng cách sử dụng framework MUI. Trước tiên, tôi sẽ trình bày toàn bộ mã và giải thích theo từng phần.

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> )
}

Nếu bạn muốn biến biểu mẫu thành cửa sổ bật lên thay vì một trang riêng biệt, hãy thêm thuộc tính isOpen() và isClosed() . Thuộc tính onSubmit() là bắt buộc vì điều này sẽ kích hoạt hàm createPOI() và thêm một đối tượng mới vào danh sách.

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

Để xác thực biểu mẫu người dùng, chúng ta sẽ cài đặt và nhập Zod. Tại đây, hãy khai báo định dạng đầu vào cần có và có yêu cầu nào như độ dài tối thiểu hoặc tối đa không.

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(),
})

Bên trong thành phần, chúng ta cần triển khai lệnh gửi và xác thực người dùng.

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

Các lỗi sẽ được triển khai trong TextField của biểu mẫu cùng với bất kỳ thuộc tính nào khác.

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

Đảm bảo rằng biểu mẫu có thể được đóng và gửi ngay từ đầu.

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

Bạn có thể triển khai cửa sổ bật lên này trong một thành phần khác.

import { Fab } from '@mui/material'
import AddIcon from '@mui/icons-material/Add' <Fab size='large' aria-label='add' onClick={() => setIsDialogOpen(true)}
> <AddIcon />
</Fab>

Xóa các mục

Tạo một hook sử dụng DELETE và triển khai nó trong một thành phần.

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, };
}
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 /> </> )
}

Lọc các mục

Bên trong thành phần tạo một nút chuyển đổi cho đầu vào văn bản bộ lọc và một hằng số lọc các điểm tham quan dựa trên nhóm tuổi hoặc thẻ. Chuỗi tùy chọn (?) đảm bảo nó xử lý các giá trị null hoặc không xác định mà không có lỗi.

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()), )

Bao gồm nó khi lặp qua danh sách các mục.

{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>
))}

Phần kết luận

Sử dụng React với TypeScript cho phép bạn xây dựng các trang web động, an toàn, dễ bảo trì và mở rộng. Kiểm tra kiểu của TypeScript ngăn ngừa lỗi thời gian chạy, trong khi cấu trúc dựa trên thành phần của React tổ chức dự án hiệu quả.

Cảm ơn các bạn đã theo dõi!

Bình luận

Bài viết tương tự

- vừa được xem lúc

The Twelve-Factor App, cẩm nang gối đầu giường trong xây dựng application (Phần 1)

Giới thiệu. Ngày nay các phần mềm được triển khai dưới dạng các dịch vụ, chúng được gọi là các web apps hay software-as-a-service (SaaS).

0 0 29

- vừa được xem lúc

8 Sai lầm phổ biến khi lập trình Android

1. Hard code.

0 0 185

- vừa được xem lúc

Popular interview question: What is the difference between Process and Thread? 10 seconds a day

Video được đăng tại channel Tips Javascript

0 0 28

- vừa được xem lúc

Thuật toán và ứng dụng - P1

Mục đích series. . Những bài toán gắn liền với thực tế. Từ đó thấy được tầm quan trọng của thuật toán trong lập trình.

0 0 35

- vừa được xem lúc

Tác dụng của Docker trong quá trình học tập

Docker bây giờ gần như là kiến thức bắt buộc đối với các anh em Dev và Devops, nhưng mà đối với sinh viên IT nói chung vẫn còn khá mơ hồ và không biết tác dụng thực tế của nó. Hôm nay mình sẽ chia sẻ

0 0 30

- vừa được xem lúc

Làm giàu trong ngành IT

Hầu như mọi người đều đi làm để kiếm tiền, ít người đi làm vì thấy cái nghề đó thú vị lắm. Bây giờ vất cho mình 100 tỷ bảo mình bỏ nghề thì mình cũng bỏ thôi.

0 0 32