Tìm hiểu về Data-flow trong Remix framework

Phat Truong-avatar

Phat Truong

2022/12/26
Hôm nay chúng ta sẽ cùng nhau tìm hiểu cơ bản về State và Data-Flow trong Remix. Nó có những điểm gì cần lưu ý? Remix quản lý "state" trên Client-side như thế nào, Remix sync-up state giữa hai phía client-side và server-side như thế nào, nó khác gì so với một ứng dụng thuần React?. Ok let's go! 😊
Tìm hiểu về Data-flow trong Remix framework

React View, action và state

Trước tiên chúng ta hãy cùng nhau ôn lại một chút kiến thức React. Rất là basic thôi, React sẽ update UI khi state thay đổi, state thay đổi khi nó nhận được một event (tạm gọi là action )từ phía người dùng. React sẽ quản lý tất cả, từ lắng nghe sự kiện và quản lý state thay đổi và trigger update UI dựa trên sự thay đổi của state. Dựa vào những điều trên chúng ta sẽ có công thức:

React UI = fn(state);

React UI là kết quả của một function logic với param là State. Với React UI tạm hiểu là Virtual-DOM, fn(state) là function quản lý state (ví dụ như useState hay useReduce vậy), chịu trách nhiệm nhận và update state dựa vào actions nào đó. Chúng ta sẽ xem xét fn(state) như là một State management vậy.

Với công thức này React đã giới thiệu cho chúng ta "one way data-flow": từ UI => trigger một action => update state => update UI. Nếu bạn chưa hình dung rõ chỗ này thì hãy đọc lại phần Thinking-in-React nhé, có ví dụ rất cơ bản và cụ thể. Trích đoạn từ Thinking-in-React

The component at the top of the hierarchy will take your data model as a prop. If you make a change to your underlying data model and call `root.render()` again, the UI will be updated. You can see how your UI is updated and where to make changes. **React’s one-way data flow (also called one-way binding) keeps everything modular and fast.**

Với ý tưởng về "one-way data flow", React giúp chúng ta xây dựng được một ứng dụng dễ hiểu, trực quan và dễ debug hơn rất nhiều. UI chỉ thay đổi khi state thay đổi, không có một side-effect nào ảnh hưởng tới quá trình thay đổi của UI, đây là một vòng tròn khép kín và thống nhất nên rất dễ tracking và fix bug.

view-state-action

Vậy nếu "state" nằm ở phía Server thì sẽ như thế nào 🤔

Với kiến trúc React "one way data-binding" thì mọi state đều được quản lý, lưu trữ và tồn tại duy nhất ở phía "client" và tất cả mọi event action đều là đồng bộ (sync). User có thể làm bất kỳ hành động, thay đổi data, thay đổi state như thế nào thì React đều có thể đám ứng được nhưng những data/state mà user đã khởi tạo chỉ có thể tồn tại ở phía client, nếu refresh page, tất cả mọi thứ sẽ trở về ban đầu. Bạn hãy tưởng tượng như todo-app vậy, bạn tạo n-todo items nhưng khi refresh thì tất cả sẽ biến mất.

Ok! có thể bạn lưu tạm data ở localStorage hoặc session, nhưng đó chỉ là lưu tạm và bạn không thể chia sẻ cho một user khác. Và hầu hết tất cả các modern application, website, trang quản lý, thương mại điện tử,... bạn cần phải lưu data và đồng bộ nó để có thể dùng ở tất cả mọi nơi. Yes, bạn cần có một Server, một database, nơi bạn có thể lưu trữ data, sync-up data đó vào React application của bạn. React "one way data-binding" làm rất tốt ở phía client nơi data/state được khởi tạo và thực thi nhưng nếu data/state đó ở server thì mọi chuyện lại không đơn giản như thế. :D

react-làm-việc-với-database

React application làm việc với server database lúc này cũng cần phải deal với remote-data, deal với side-effects (những sự kiện nằm bên ngoài application nhưng lại ảnh hưởng đến data và trong nhiều trường hợp cần update state, UI), cần phải xử lý bất đồng bộ (async). Lúc này React hook đã giới thiệu useEffect để "xử lý riêng" những khu vực bất đồng bộ đó. Nhưng useEffect cực kỳ rất dễ hiểu lầm và đa phần chúng ta đều đang lạm dụng nó (Đọc thêm ở đây nhé 😗 You Might Not Need an Effect)

react-làm-việc-với-database-error

Hầu hết các State Management library đều có giải pháp để xử lý bất đồng bộ và giúp React có thể sync-up được với database nhưng, như đã nói tất cả chỉ có thể làm việc được trên phía client và xử lý những tác phụ async hoặc sync-up dù có sử dụng lib hay không đều không dễ. Vẫn còn đó khoảng cách giữa Client-side và Server-side dù dùng chung một database, rất dễ xảy ra bug khi database / state từ hai phía không được sync-up đồng thời. Bạn cứ tưởng tượng khoảng cách này là một động Bugs vậy 🤣🤣🤣

Dễ lộn cái bàn lắm !! (ノಠ益ಠ)ノ彡┻━┻

Remix data flow

Với Remix, với cấu trúc và bằng việc gộp cả hai phía Server-side để xử lý logic; Client-side sử dụng React để render UI; Remix đã mở rộng khái niệm "one way data-binding" lên trên môi trường network: mang Data (State ) từ phía Server-side lên trên Client-side (View) và mang Data/State trở ngược lại phía Server-side thông qua một event (Action) nào đó.

Remix-one-data-flow

Nói rõ thêm về chỗ này một chút, State mà chúng ta đề cập nãy giờ ở đây mình gọi là remote-state (ví dụ như là thông tin user, thông tin sản phẩm, notification, ... tất cả những data mà bạn cần lưu ở phía database). Remote-state khác với local-state (bạn có thể tạm hiểu là state sinh ra cho mục đích thay đổi UI nhưng không ảnh hưởng đến data ví dụ như đóng/mở popup, hoặc thay đổi theme vậy), remote-state sẽ không thay đổi trừ khi bạn send một action đến Server còn local-state khi bạn f5 thì mọi thứ sẽ biến mất và quay về trạng thái ban đầu. Local-state thì chỉ nên tồn tại bên trong React component thôi, state này sẽ bị mất khi f5 và không có lý do gì để chúng ta gửi nó đến server để lưu vào database đúng không nào^^ . Còn state ở Remix app mình nói ở đây chính là remote-state nhé ^^.

Remix-remote-state

Với cấu trúc này, khoảng cách giữa hai phía là không còn vì mỗi bên đảm nhận một nhiệm vụ riêng biệt và cả hai phía đều dùng chung một remote-state (bạn cũng có thể hiểu là: Single-source-of-trust). State giờ đây nằm ở phía Server-side và React có nhiệm vụ render View UI tương ứng. Với Remix, chúng ta không còn phải bận tâm so sánh remote-state giữa hai phía đang có giống nhau hay khác nhau như thế nào nữa vì React giờ đây chỉ đơn giản nhận remote-state và render tương ứng.

Remix in action

Ok, lý thuyết võng vo nãy h đủ hoa mắt rồi @@ , jump to code coi thử có gì nào.

// TimeSheetItem.js

/**
 * "Loader" function. Function được thực thi ở phía Server-side.
 * Bạn có thể hiểu rằng đây là function chuẩn bị data remote-state cho phía View vậy
 */
export const loader = async ({ params }) => {
  const { timeSheetId } = params;
  const timeSheetObj = await getTimeSheet(timeSheetId);
  
  return json({ timeSheetObj });
}

/**
 * "View" function component. Đây là React component được Render -> HTML-DOM ở phía Server-side.
 * Đây chính là "View" mà chúng ta đề cập trong bài viết này ^^
 */
const View = () => {
    // Lấy data đã được chuẩn ở `loader` function phía trên
    const { timeSheetObj } = useLoaderData(); // remote-state.

    // local-state. Chỉ đảm nhận update trên UI mà thôi.
    // Không làm ảnh hưởng đến data từ phía server.
    // Khi refresh, giá trị của "isExpandViewMore" luôn luôn về mặc định là "false".
    const [isExpandViewMore, setViewMoreExpand] = useState(false);

    return (
        <div>
            <div>{timeSheetObj.title}</div>
            <div>{timeSheetObj.time}</div>
            <div>
                {timeSheetObj.content}
            </div>
            <div>{timeSheetObj.isPublish}</div>
             setViewMoreExpand(prev =&gt; !prev)}&gt;
                View more
            

            {/* Form submit */}
            
                
                
                    Make Publish
                
            
        </div>
    );
};

/**
 * "action" function. Function được thực thi ở phía Server-side
 * Bạn có thể tạm hiểu rằng đây là function xử lý và update lại data state khi nhận được action từ phía View
 * Tất nhiên bạn có thể trả về data state đã được xử lý lại cho phía View
 * Và phía View tất nhiên sẽ nhận được data từ function action này thông qua function "loader"
 */
export const action = async ({ request }) =&gt; {
    const form = await request.formData();
    const timeSheetId = form.get('timeSheetId');

    const timeSheetUpdated = await setPublishTimeSheet(timeSheetId);

    return redirect(`timesheet/${timeSheetId}`);
};

Chúng ta sẽ có modal cơ bản về Remix là như thế này.

Remix modal

Vẫn với công thức React = fn(state) nhưng với Remix thì state ở đây chính là đến từ phía server-side, chính là database model hay nói cách khác Remix State là đến từ network, React với Remix bây giờ chỉ còn mỗi nhiệm vụ View, nhận và render tương ứng (thông qua function "loader" ở trên, Remix chịu trách nhiệm khu vực này để UI của bạn lun up-to-date, chớ lo hén (✦ ‿ ✦). Cấu trúc của Remix là phần bổ sung và cũng là hoàn thiện giúp cho data state luôn là một thể thống nhất, là "single source of trust" giữa tầng View và Server. Không còn chuyện data ở server một đằng nhưng User thấy trên UI là một nẻo nữa nhé.

( ˘▽˘)っ♨

Vậy còn state management thì sao 🤔

Hmmmm, như bài viết mình đã đề cập rằng Remix state đến từ phía Server-side và React component hoặc View chỉ làm nhiệm vụ render data tương ứng; lúc này React đã trở thành Dummy component, nhận render không có side-effect, không cần làm thêm gì nữa cả. Lúc này, mình có thể mạnh dạn nói với bạn rằng State management với Remix thì là đã có rồi, nó đã tồn tại rồi, không đâu xa đó chính là Server và database của bạn, nó vốn là vậy rồi. Trong hầu hết các ứng dụng cần dynamic data như Shopping, Blog, Gallery, không cần phải build thêm một State management nào khác đâu. Trước khi xây dựng cho State management trong ứng dụng sử dụng Remix, hãy suy nghĩ về app bạn đang xây dựng, hãy suy nghĩ về những việc Remix có thể làm để delivery data, optimize UI,...hãy suy nghĩ kỹ về State management và tự đặt câu hỏi rằng có thực sự cần thiết hay không nhé. ⊂◉‿◉つ


Bài viết này mình tham khảo ở đây: Data Flow in Remix - Jim Nielsen

Cảm ơn các bạn đã đọc bài. Chúc các bạn thành công. Hẹn gặp ở next episode. Pipi.

(づ ◕‿◕ )づ

Relative posts