Server Actions trong Next.js 14

I.Giới thiệu

Next.js là một framework phổ biến trong việc phát triển ứng dụng React, nổi bật với khả năng kết hợp giữa rendering phía client và server. Phiên bản mới nhất của Next.js, phiên bản 14, đã giới thiệu một tính năng đột phá mang tên Server Actions. Trong bài viết này, chúng ta sẽ khám phá chi tiết về Server Actions, từ định nghĩa, cách sử dụng, đến các ưu và nhược điểm, cũng như các vấn đề bảo mật liên quan và cách khắc phục.

II. Chi tiết

1. Định nghĩa Server Actions

Server Actions là một tính năng mới trong Next.js 14, cho phép lập trình viên viết các hành động server trực tiếp trong mã React. Thay vì phải tách biệt rõ ràng giữa phần code client và server, Server Actions giúp bạn kết hợp chúng một cách liền mạch, làm cho việc quản lý logic ứng dụng trở nên dễ dàng hơn.

2. Tại sao phải viết code backend trong React?

Việc viết code backend trong React giúp giảm bớt sự phức tạp khi cần phải chuyển dữ liệu qua lại giữa server và client. Nó cho phép bạn sử dụng cùng một ngôn ngữ và cấu trúc code để xử lý cả logic phía server và phía client, giúp giảm thiểu lỗi và tăng cường sự nhất quán trong ứng dụng.

3. Từ khoá use-server

use-server là một từ khóa mới trong Next.js 14, được sử dụng để xác định các hành động sẽ được thực thi trên server. Khi bạn đánh dấu một hàm với use-server, Next.js sẽ hiểu rằng hàm này cần được thực thi trên server và không gửi nó đến client.

use-server có các thuộc tính chính sau:

  1. Serialization: Tự động serializes dữ liệu để gửi từ server tới client.
  2. Contextual Awareness: Có thể truy cập vào các thông tin về yêu cầu HTTP hiện tại như headers, cookies.
  3. Automatic Code Splitting: Chỉ gửi phần code cần thiết tới client, giúp tối ưu hoá hiệu suất.

4. Ví dụ về cách gọi API truyền thống và cách sử dụng Server Actions

Ví dụ 1: Gọi API truyền thống
// pages/api/users.js
import { connectToDatabase } from '../../utils/mongodb';

export default async function handler(req, res) {
    const { db } = await connectToDatabase();
    const users = await db.collection('users').find({}).toArray();
    res.status(200).json(users);
}
// components/TraditionalFetch.js
import React, { useState, useEffect } from 'react';

const TraditionalFetch = () => {
    const [users, setUsers] = useState([]);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
        const fetchUsers = async () => {
            const response = await fetch('/api/users');
            const data = await response.json();
            setUsers(data);
            setLoading(false);
        };

        fetchUsers();
    }, []);

    return (
        <div>
            {loading ? (
                <p>Loading...</p>
            ) : (
                <ul>
                    {users.map(user => (
                        <li key={user._id}>{user.name}</li>
                    ))}
                </ul>
            )}
        </div>
    );
};

export default TraditionalFetch;

Trong ví dụ gọi API truyền thống:

  • Chúng ta có một API route tại file pages/api/users.js. Trong file này, chúng ta kết nối đến một cơ sở dữ liệu MongoDB thông qua một hàm connectToDatabase được định nghĩa sẵn. Sau đó, chúng ta truy xuất tất cả các tài liệu từ collection users và trả về dữ liệu dưới dạng JSON.
  • Ở phía client, chúng ta có component TraditionalFetch.js, nơi chúng ta sử dụng hook useEffect để thực hiện việc gọi API khi component được render lần đầu tiên. Chúng ta sử dụng hàm fetch để gọi endpoint /api/users, sau đó cập nhật state users với dữ liệu nhận được và thay đổi state loading để hiển thị danh sách người dùng hoặc thông báo "Loading..." trong khi dữ liệu đang được tải.
Ví dụ 2: Sử dụng Server Actions với SQL
// utils/db.js
import { Pool } from 'pg';

const pool = new Pool({
    user: 'yourusername',
    host: 'localhost',
    database: 'yourdatabase',
    password: 'yourpassword',
    port: 5432,
});

export const query = (text, params) => pool.query(text, params);
// components/ServerActionComponent.js
'use-server';
import React from 'react';
import { use } from 'react';
import { query } from '../utils/db';

// Định nghĩa hành động server
async function fetchUsers() {
    const { rows } = await query('SELECT * FROM users');
    return rows;
}

const ServerActionComponent = () => {
    const users = use(fetchUsers);

    return (
        <div>
            <ul>
                {users.map(user => (
                    <li key={user.id}>{user.name}</li>
                ))}
            </ul>
        </div>
    );
};

export default ServerActionComponent;

Trong ví dụ sử dụng Server Actions:

  • chúng ta định nghĩa một utility để kết nối đến cơ sở dữ liệu PostgreSQL tại file utils/db.js bằng cách sử dụng module pg.
  • Chúng ta thiết lập kết nối với các thông tin cấu hình được truyền vào từ các biến môi trường để bảo mật thông tin nhạy cảm.
  • Tiếp theo, trong component ServerActionComponent.js, chúng ta sử dụng từ khoá 'use-server' để chỉ định rằng các hành động trong hàm fetchUsers sẽ được thực hiện trên server. Hàm fetchUsers thực hiện một truy vấn SQL để lấy dữ liệu người dùng từ cơ sở dữ liệu và trả về kết quả.
  • Chúng ta sử dụng hook use để gọi hàm fetchUsers và nhận dữ liệu ngay khi component được render. Sau đó, chúng ta hiển thị danh sách người dùng trên giao diện.
So sánh hiệu suất và chi tiết

Hiệu suất:

  • Truyền thống: Mỗi lần component TraditionalFetch được render, sẽ có một request từ client tới server để lấy dữ liệu. Điều này tạo ra một độ trễ nhất định và phụ thuộc vào tốc độ mạng giữa client và server.
  • Server Actions: Dữ liệu được lấy trực tiếp từ server khi component ServerActionComponent được render, giảm thiểu số lượng request và tối ưu hóa hiệu suất. Thời gian phản hồi chủ yếu phụ thuộc vào tốc độ truy vấn database.

Quản lý code:

  • Truyền thống: Phải quản lý cả API route và component, làm cho code trở nên phân tán và khó bảo trì hơn.
  • Server Actions: Logic server và client được kết hợp trong một nơi, giúp đơn giản hóa cấu trúc ứng dụng và dễ bảo trì hơn.

Tính năng động:

  • Truyền thống: Việc fetch dữ liệu diễn ra trên client, cho phép người dùng tương tác với ứng dụng trong quá trình dữ liệu đang được tải.
  • Server Actions: Dữ liệu được tải trước khi render, làm cho trải nghiệm người dùng mượt mà hơn, nhưng có thể làm tăng thời gian phản hồi ban đầu.

Bảo mật:

  • Truyền thống: API route có thể dễ dàng được bảo mật bằng cách thêm các middleware cho xác thực và phân quyền.
  • Server Actions: Cần thiết lập các biện pháp bảo mật trong chính hàm hành động server, nhưng giảm thiểu khả năng bị tấn công do không có endpoint rõ ràng cho client truy cập.

Nhìn chung, Server Actions cung cấp một cách tiếp cận mới mẻ và hiệu quả trong việc phát triển ứng dụng React với Next.js, tuy nhiên cần cẩn thận về mặt bảo mật và quản lý tài nguyên server.

5. Ưu điểm và nhược điểm của Server Actions

Ưu điểm
  1. Đơn giản hóa cấu trúc ứng dụng: Sử dụng Server Actions cho phép bạn kết hợp logic phía server và client trong cùng một file hoặc module. Điều này giúp giảm thiểu sự phân tán và phức tạp của mã nguồn, làm cho việc đọc và bảo trì code trở nên dễ dàng hơn.
  2. Tối ưu hiệu suất: Với Server Actions, dữ liệu được truy xuất trực tiếp trên server và chỉ gửi kết quả cần thiết về client. Điều này giảm thiểu số lượng request HTTP và cải thiện tốc độ phản hồi của ứng dụng, đặc biệt quan trọng đối với các ứng dụng có yêu cầu cao về thời gian thực.
  3. Cải thiện bảo trì: Vì logic phía server và client được quản lý cùng nhau, việc cập nhật hoặc thay đổi logic trở nên dễ dàng hơn. Điều này giúp giảm thiểu lỗi phát sinh khi có sự thay đổi ở một phía mà không đồng bộ với phía còn lại.
Nhược điểm
  1. Hạn chế trong môi trường client: Vì Server Actions chạy trên server, chúng không thể tận dụng tài nguyên của client để giảm tải cho server. Điều này có thể tạo ra giới hạn trong việc xử lý một số tác vụ mà có thể được thực hiện trên client để tăng hiệu quả. Ví dụ như trong một ứng dụng xử lý ảnh, việc chỉnh sửa ảnh có thể được thực hiện trên client để giảm tải cho server. Tuy nhiên, nếu sử dụng Server Actions, tất cả tác vụ này sẽ phải thực hiện trên server, tăng tải cho hệ thống server.
  2. Tăng tải cho server: Di chuyển nhiều logic xử lý lên server có thể làm tăng tải cho server, đặc biệt nếu ứng dụng của bạn có lượng người dùng lớn hoặc yêu cầu xử lý phức tạp. Điều này yêu cầu quản lý tài nguyên server một cách hiệu quả để tránh tình trạng quá tải.
  3. Phụ thuộc vào Next.js: Các ứng dụng sử dụng mạnh tính năng Server Actions sẽ khó chuyển đổi sang framework khác nếu cần thiết, do phụ thuộc sâu vào cách Next.js xử lý Server Actions. Điều này có thể gây khó khăn nếu bạn muốn thay đổi framework hoặc nếu Next.js không còn phù hợp với nhu cầu phát triển của bạn. Nếu bạn bắt đầu phát triển một ứng dụng với Next.js và sử dụng Server Actions để quản lý logic, sau này nếu muốn chuyển sang một framework khác như Nuxt.js (dành cho Vue.js) vì lý do kỹ thuật hoặc yêu cầu dự án, việc chuyển đổi sẽ phức tạp và tốn nhiều công sức.

6. Vấn đề bảo mật khi sử dụng Server Actions

Các vấn đề bảo mật
  1. Truy cập trái phép: Nếu không được kiểm soát chặt chẽ, các hàm Server Actions có thể bị truy cập trái phép.
  2. Injection Attacks: Các hàm Server Actions có thể trở thành mục tiêu của các cuộc tấn công SQL Injection hoặc XSS nếu không được bảo vệ đúng cách.
  3. Data Leakage: Việc truyền dữ liệu nhạy cảm từ server đến client có thể dẫn đến rò rỉ dữ liệu nếu không được mã hóa đúng cách.
Giải pháp bảo mật
  1. Xác thực và phân quyền: Đảm bảo chỉ có những người dùng đã xác thực và có quyền mới có thể truy cập vào các hành động server.
  2. Validation và Sanitization: Luôn validate và sanitize dữ liệu đầu vào để ngăn chặn các cuộc tấn công injection.
  3. Mã hóa dữ liệu: Sử dụng các giao thức mã hóa như HTTPS để bảo vệ dữ liệu truyền giữa client và server.
  4. Kiểm tra bảo mật thường xuyên: Thực hiện kiểm tra bảo mật thường xuyên để phát hiện và khắc phục các lỗ hổng bảo mật kịp thời.

III. Kết luận

Server Actions trong Next.js 14 mang lại nhiều lợi ích trong việc phát triển ứng dụng React hiện đại, giúp tối ưu hóa hiệu suất và đơn giản hóa cấu trúc ứng dụng. Tuy nhiên, để tận dụng tối đa lợi ích này, cần phải lưu ý đến các vấn đề bảo mật và có biện pháp bảo vệ phù hợp. Hy vọng bài viết này đã giúp bạn hiểu rõ hơn về Server Actions và cách sử dụng nó một cách hiệu quả trong dự án của mình.

IV. References

https://unicorn-utterances.com/posts/what-are-react-server-actions#Why-are-React-Server-Actions-significant

https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations

https://blog.logrocket.com/diving-into-server-actions-next-js-14/