Bài 25.5: Bài tập thực hành React-GraphQL

Bài 25.5: Bài tập thực hành React-GraphQL
Chào mừng trở lại với series Lập trình Web Front-end! Sau khi đã khám phá những khái niệm quan trọng về GraphQL và cách nó định hình lại cách chúng ta tương tác với dữ liệu, giờ là lúc chúng ta mang sức mạnh đó vào ứng dụng React của mình. Lý thuyết là nền tảng, nhưng thực hành mới là chìa khóa để nắm vững và áp dụng hiệu quả.
Bài viết này sẽ tập trung vào việc thực hành các tác vụ cốt lõi khi làm việc với GraphQL trong React: truy vấn (fetching) và thay đổi (mutating) dữ liệu. Chúng ta sẽ sử dụng Apollo Client - một thư viện phổ biến và mạnh mẽ giúp tích hợp GraphQL vào các ứng dụng JavaScript ở phía client.
Hãy cùng xắn tay áo lên và bắt đầu nào!
Chuẩn bị môi trường
Để thực hành, bạn cần có:
- Một ứng dụng React đã được thiết lập.
- Apollo Client đã được cài đặt và kết nối với GraphQL server của bạn. Nếu bạn chưa có server, bạn có thể sử dụng các dịch vụ GraphQL công cộng hoặc tự tạo một server đơn giản cho mục đích thực hành.
Giả sử bạn đã có cấu hình cơ bản với ApolloProvider
bọc quanh ứng dụng của mình:
// src/index.js hoặc src/App.js import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client'; const client = new ApolloClient({ uri: 'YOUR_GRAPHQL_ENDPOINT', // Thay thế bằng địa chỉ GraphQL server của bạn cache: new InMemoryCache(), }); const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <React.StrictMode> <ApolloProvider client={client}> <App /> </ApolloProvider> </React.StrictMode> );
Với cấu hình này, bất kỳ component con nào bên trong ApolloProvider
đều có thể sử dụng các hook của Apollo Client để tương tác với GraphQL API.
Thực hành 1: Truy vấn dữ liệu đơn giản (useQuery
)
Task đầu tiên và quan trọng nhất là lấy dữ liệu từ GraphQL server. Apollo Client cung cấp hook useQuery
giúp việc này trở nên cực kỳ đơn giản và trực quan.
Hãy tưởng tượng chúng ta muốn hiển thị danh sách các bài viết.
Đầu tiên, định nghĩa GraphQL query của bạn bằng cách sử dụng gql
tag:
// src/queries.js hoặc trực tiếp trong component
import { gql } from '@apollo/client';
export const GET_POSTS = gql`
query GetPosts {
posts {
id
title
author {
name
}
}
}
`;
Giải thích:
gql
tag giúp phân tích chuỗi GraphQL query của bạn.query GetPosts
là tên của hoạt động GraphQL, hữu ích cho việc debugging.- Bên trong
{ }
, chúng ta chỉ định các trường (posts
) và các trường con (id
,title
,author
,name
) mà chúng ta thực sự cần từ server. Đây chính là sức mạnh của GraphQL - chỉ lấy những gì bạn cần!
Tiếp theo, sử dụng useQuery
trong component React của bạn:
import React from 'react'; import { useQuery } from '@apollo/client'; import { GET_POSTS } from './queries'; // Import query đã định nghĩa function PostsList() { const { loading, error, data } = useQuery(GET_POSTS); if (loading) return <p>Đang tải danh sách bài viết...</p>; if (error) return <p>Có lỗi xảy ra khi tải bài viết: {error.message}</p>; // data sẽ có cấu trúc giống với query: { posts: [...] } return ( <div> <h2>Danh sách bài viết</h2> <ul> {data.posts.map(post => ( <li key={post.id}> **{post.title}** bởi *{post.author.name}* </li> ))} </ul> </div> ); } export default PostsList;
Giải thích:
useQuery(GET_POSTS)
thực thi queryGET_POSTS
khi component được render.- Hook trả về một object với các thuộc tính quan trọng:
loading
: Làtrue
khi request đang được gửi đi. Sử dụng để hiển thị trạng thái "đang tải".error
: Chứa thông tin lỗi nếu có bất kỳ vấn đề nào xảy ra trong quá trình fetching.data
: Chứa dữ liệu thành công nhận được từ server, có cấu trúc giống với query bạn đã định nghĩa.
- Chúng ta xử lý các trạng thái
loading
vàerror
trước khi render dữ liệu. - Khi có dữ liệu, chúng ta duyệt qua
data.posts
và hiển thị từng bài viết.
Đây là cách cơ bản nhất để lấy dữ liệu với GraphQL trong React sử dụng Apollo Client. Nó đơn giản, hiệu quả và dễ đọc.
Thực hành 2: Truy vấn dữ liệu với Biến (useQuery
with variables)
Thông thường, chúng ta cần lấy dữ liệu cụ thể dựa trên một điều kiện nào đó, ví dụ lấy thông tin chi tiết của một bài viết dựa vào ID. Lúc này, chúng ta sử dụng biến (variables) trong GraphQL query.
Đầu tiên, định nghĩa query với biến:
// src/queries.js
import { gql } from '@apollo/client';
export const GET_POST_BY_ID = gql`
query GetPostById($postId: ID!) {
post(id: $postId) {
id
title
body
author {
name
}
}
}
`;
Giải thích:
$postId: ID!
khai báo một biến có tên$postId
, kiểuID
và là bắt buộc (!
).post(id: $postId)
sử dụng biến$postId
làm đối số cho trườngpost
.
Tiếp theo, sử dụng useQuery
và truyền biến vào:
import React from 'react'; import { useQuery } from '@apollo/client'; import { GET_POST_BY_ID } from './queries'; function PostDetail({ postId }) { // Component nhận postId làm prop const { loading, error, data } = useQuery(GET_POST_BY_ID, { variables: { postId: postId }, // Truyền biến vào đây // hoặc viết tắt: variables: { postId }, nếu tên prop và tên biến giống nhau }); if (loading) return <p>Đang tải chi tiết bài viết...</p>; if (error) return <p>Có lỗi xảy ra khi tải bài viết: {error.message}</p>; if (!data || !data.post) return <p>Không tìm thấy bài viết này.</p>; const { post } = data; return ( <div> <h1>{post.title}</h1> <p>Bởi *{post.author.name}*</p> <hr /> <div>{post.body}</div> </div> ); } export default PostDetail;
Giải thích:
- Đối số thứ hai của
useQuery
là một object cấu hình. - Thuộc tính
variables
trong object này nhận một object khác, trong đó key là tên biến (không có$
) và value là giá trị của biến đó. - Khi
postId
thay đổi,useQuery
sẽ tự động thực thi lại query với biến mới.
Đây là cách linh hoạt để lấy dữ liệu động dựa trên input của người dùng hoặc trạng thái của ứng dụng.
Thực hành 3: Thay đổi dữ liệu (useMutation
)
Ngoài việc lấy dữ liệu, chúng ta cũng cần thay đổi dữ liệu trên server (thêm, sửa, xóa). Trong GraphQL, các hoạt động này được gọi là mutations. Apollo Client cung cấp hook useMutation
cho mục đích này.
Hãy tạo một form để thêm bài viết mới.
Đầu tiên, định nghĩa GraphQL mutation:
// src/mutations.js hoặc trực tiếp trong component
import { gql } from '@apollo/client';
export const CREATE_POST_MUTATION = gql`
mutation CreatePost($title: String!, $body: String!) {
createPost(title: $title, body: $body) {
id
title
# Có thể yêu cầu trả về thêm các trường khác nếu cần
}
}
`;
Giải thích:
mutation CreatePost(...)
khai báo một mutation.$title: String!, $body: String!
định nghĩa các biến input cho mutation.createPost(title: $title, body: $body)
gọi đến trườngcreatePost
trên server và truyền biến vào.- Bên trong
{ }
saucreatePost
, chúng ta yêu cầu server trả vềid
vàtitle
của bài viết vừa được tạo sau khi mutation thành công. Điều này rất hữu ích!
Tiếp theo, sử dụng useMutation
trong component React của bạn (chẳng hạn một form):
import React, { useState } from 'react'; import { useMutation } from '@apollo/client'; import { CREATE_POST_MUTATION } from './mutations'; // Có thể cần import lại GET_POSTS để cập nhật cache/list sau khi tạo // import { GET_POSTS } from './queries'; function CreatePostForm() { const [title, setTitle] = useState(''); const [body, setBody] = useState(''); // useMutation trả về một mảng: [hàm trigger mutation, kết quả mutation] const [createPost, { loading, error, data }] = useMutation(CREATE_POST_MUTATION, { // **Lưu ý:** Cần xử lý cập nhật cache hoặc refetch data sau mutation để UI đồng bộ // Option 1: Refetch một query cụ thể // refetchQueries: [ // { query: GET_POSTS }, // Refetch lại danh sách bài viết // 'GetPosts', // Hoặc chỉ dùng tên query nếu bạn đặt tên ở query GET_POSTS // ], // Option 2: Cập nhật cache trực tiếp (phức tạp hơn nhưng hiệu quả hơn) // update(cache, { data: { createPost } }) { // const existingPosts = cache.readQuery({ query: GET_POSTS }); // if (existingPosts && existingPosts.posts) { // cache.writeQuery({ // query: GET_POSTS, // data: { posts: [...existingPosts.posts, createPost] }, // }); // } // } }); const handleSubmit = async (e) => { e.preventDefault(); try { // Gọi hàm trigger mutation và truyền biến vào const response = await createPost({ variables: { title, body } }); console.log('Bài viết đã được tạo:', response.data.createPost); // Reset form setTitle(''); setBody(''); alert(`Bài viết "${response.data.createPost.title}" đã được tạo thành công!`); } catch (err) { console.error('Lỗi khi tạo bài viết:', err); alert('Đã xảy ra lỗi khi tạo bài viết.'); } }; return ( <form onSubmit={handleSubmit}> <h3>Tạo bài viết mới</h3> <div> <label htmlFor="title">Tiêu đề:</label> <input id="title" type="text" value={title} onChange={(e) => setTitle(e.target.value)} required /> </div> <div> <label htmlFor="body">Nội dung:</label> <textarea id="body" value={body} onChange={(e) => setBody(e.target.value)} required /> </div> <button type="submit" disabled={loading}> {loading ? 'Đang tạo...' : 'Tạo bài viết'} </button> {error && <p style={{ color: 'red' }}>Lỗi: {error.message}</p>} {/* data ở đây chứa kết quả trả về từ mutation nếu thành công */} {/* {data && <p style={{ color: 'green' }}>Tạo thành công! ID: {data.createPost.id}</p>} */} </form> ); } export default CreatePostForm;
Giải thích:
useMutation(CREATE_POST_MUTATION)
trả về một mảng:- Phần tử đầu tiên (
createPost
) là hàm mà bạn gọi để thực thi mutation. - Phần tử thứ hai là một object chứa trạng thái của mutation (
loading
,error
,data
).
- Phần tử đầu tiên (
- Chúng ta định nghĩa hàm
handleSubmit
để xử lý sự kiện submit form. - Trong
handleSubmit
, chúng ta gọicreatePost({ variables: { title, body } })
để gửi mutation đi, truyền dữ liệu form vào biến. - Sử dụng
async/await
để chờ kết quả từ mutation. - Xử lý thành công (reset form, thông báo) và lỗi.
- Đặc biệt quan trọng: Sau khi mutation thành công, dữ liệu trên server đã thay đổi. Bạn cần đảm bảo giao diện người dùng (ví dụ: danh sách bài viết) được cập nhật để phản ánh sự thay đổi này. Các phương pháp phổ biến là
refetchQueries
(đơn giản nhưng có thể không hiệu quả) hoặcupdate
cache (phức tạp hơn nhưng tối ưu).
Các khía cạnh quan trọng khác cần thực hành
Khi bạn đã thành thạo việc fetch và mutate cơ bản, hãy tiếp tục khám phá các khía cạnh khác của React-GraphQL:
- Xử lý lỗi nâng cao: Hiển thị thông báo lỗi cụ thể hơn cho người dùng, xử lý các loại lỗi khác nhau từ server.
- Quản lý trạng thái loading: Hiển thị skeleton screen hoặc spinner thích hợp thay vì chỉ dòng chữ "Đang tải...".
- Cập nhật Cache (
update
function): Học cách sử dụng hàmupdate
tronguseMutation
để cập nhật cache Apollo một cách hiệu quả sau khi thêm, sửa, xóa dữ liệu. Điều này giúp giao diện người dùng phản hồi nhanh chóng mà không cần refetch toàn bộ dữ liệu. - Phân trang (Pagination): Xử lý danh sách dữ liệu lớn bằng cách lấy từng phần (ví dụ: 10 bài viết một lần) sử dụng biến
offset
vàlimit
hoặc các kỹ thuật cursor-based pagination. - Lấy dữ liệu hoãn lại (
useLazyQuery
): Sử dụng hook này khi bạn muốn trì hoãn việc thực thi query cho đến khi có một sự kiện cụ thể xảy ra (ví dụ: người dùng click nút tìm kiếm). - Subscriptions: Tìm hiểu cách nhận dữ liệu real-time từ server GraphQL (ví dụ: thông báo bài viết mới).
- Error Policies và Fetch Policies: Cấu hình cách Apollo Client xử lý lỗi và cách nó fetch/cache dữ liệu để tối ưu hóa hiệu suất.
Comments