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ếnmạ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ó:

  1. Một ứng dụng React đã được thiết lập.
  2. 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ảntrự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 query GET_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 loadingerror 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ả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ểu ID và là bắt buộc (!).
  • post(id: $postId) sử dụng biến $postId làm đối số cho trường post.

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ường createPost trên server và truyền biến vào.
  • Bên trong { } sau createPost, chúng ta yêu cầu server trả về idtitle 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).
  • Chúng ta định nghĩa hàm handleSubmit để xử lý sự kiện submit form.
  • Trong handleSubmit, chúng ta gọi createPost({ 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ặc update 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àm update trong useMutation để 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 offsetlimit 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

There are no comments at the moment.