Xơn Space

    My bullshit stories

    Back

    Quản lý Global State trong ứng dụng React

    logo

    Hoàng Sơn

    published at:02/08/2024 at 8:11 AM- view

    thumbail

    Như mọi người đều biết, một React App thì bao gồm rất nhiều components bên trong nó, việc quản lý dữ liệu tốt và giao tiếp giữa các components riêng lẻ này là rất quan trọng để có thể phát triển mã nguồn clean hơn và cải thiện việc chia sẻ dữ liệu giữa các components.

    Quản lý Global State được tạo ra như một cách để tập trung và quản lý dữ liệu trong ứng dụng của mình, giúp dễ dàng thay đổi cho tất cả các components bên trong ứng dụng. Việc tìm kiếm và triển khai một cách phù hợp cho Global State là một phần không thể thiếu của một React App.

    Trong bài viết này, mình sẽ giải thích và chia sẻ góc nhìn của mình về statestate management trong React App, và discuss về những lựa chọn tốt nhất để quản lý Global State trong React App.

    1. State và Global State trong React?

    Trong các Javascript App, state đề cập đến tất cả dữ liệu mà ứng dụng cần để hoạt động. State có thể được lưu trữ dưới bất kỳ kiểu dữ liệu nào, bao gồm array, boolean, string hoặc number.

    Trong các React App, Global State là một JavaScript object, lưu trữ dữ liệu được React sử dụng để render các components với dynamic content dựa trên các action của user.

    State có thể được chia sẻ giữa các components trong các React App theo các cách khác nhau, như là chia sẻ dưới dạng Props, Global State,...

    2. State Management là gì?

    State management là một cách kiểm soát giao tiếp giữa các phần khác nhau của state và việc sử dụng chúng, bao gồm cả các React components.

    Ngoại trừ các ứng dụng cực kỳ đơn giản, components cần phải giao tiếp với nhau. Ví dụ, trong một ứng dụng thương mại điện tử, một cart component cần giao tiếp với site header component để hiển thị số lượng mặt hàng hiện có trong giỏ hàng.

    3. Tại sao State Management lại quan trọng?

    Components được sử dụng trong việc phát triển các React App, và các components này thường tự quản lý state vì một đối tượng state có thể được khởi tạo trực tiếp trong các React components.

    State của component được coi là "dữ liệu đóng gói" và chứa các thuộc tính tồn tại qua các lần render của component. Điều này hiệu quả đối với các ứng dụng có ít components, nhưng khi ứng dụng phát triển, việc kiểm soát sự phức tạp của state chia sẻ giữa các components trở nên khó khăn.

    Khi state management được triển khai:

    • Dữ liệu được hợp nhất, giúp dễ dàng hơn trong việc lý giải logic của ứng dụng.
    • Vị trí của dữ liệu luôn được biết đến.
    • Bạn có thể nhận được point-in-time snapshot của tất cả dữ liệu được lưu trữ toàn cục.
    • Quá trình phát triển của bạn sẽ tiến triển nhanh hơn.

    4. Các trường hợp sử dụng Global State Management

    Global state management hữu ích trong hầu hết các loại ứng dụng, sau đây là một vài ví dụ:

    • Ứng dụng thương mại điện tử: Trong một ứng dụng thương mại điện tử, thường có nhiều components giao tiếp với nhau và nhiều actions mà users có thể thực hiện làm thay đổi dữ liệu. Ví dụ, giỏ hàng của user, các item có thể được thêm vào và lưu lại bởi user để mua sắm sau này.
    • Ứng dụng giáo dục: Global state management hữu ích trong các ứng dụng giáo dục có nền tảng quản lý người dùng, nơi client có thể đăng nhập, đăng ký, tham gia khóa học, v.v. Tất cả những điều này đều được thực hiện nhờ state management.
    • Ứng dụng có xác thực: Nếu không có state management, sẽ rất khó để xây dựng các ứng dụng mà users cần được xác thực. Điều này là do hầu hết thông tin người dùng sẽ được yêu cầu bởi nhiều components trong ứng dụng để đảm bảo security và cung cấp cho người dùng dữ liệu tương ứng được lưu trữ trong database ứng dụng. Điều này được thực hiện nhờ state management, xác định xem người dùng đã được xác thực hay chưa và cung cấp thông tin về người dùng.

    5. Những cách để quản lý state bên trong React

    Có nhiều cách để quản lý global state trong các ứng dụng React:

    • React Hooks (ví dụ: useStateuseReducer)
    • React’s Context API
    • State Management Libraries (ví dụ: Redux, Zustand, Recoil)

    Mỗi kỹ thuật quản lý state được sử dụng để xây dựng các ứng dụng React với các quy mô khác nhau, từ quy mô nhỏ đến quy mô lớn.

    Ví dụ, dựa vào React Hooks như useStateuseReducer để quản lý global state, mặc dù được khuyến khích bởi tài liệu React Docs, nhưng nó chỉ thực sự hoạt động tốt cho các ứng dụng React nhỏ và đơn giản.

    Khi số lượng tính năng và kích thước của ứng dụng tăng lên, chúng ta bắt đầu dựa nhiều hơn vào “prop drilling” để chuyển state giữa các components và việc quản lý state của ứng dụng chỉ bằng hooks trở nên khó khăn.

    Đây là lúc Context API hoặc các state management libraries có thể phát huy tác dụng.

    5.1 React Hook

    1> useState hook:

    Với useState, bạn có thể thiết lập giá trị của state trong một component.

    const [state, setState] = useState(initialState);
    

    useState is a function that takes in the (initialState in the code above) as an argument. 

    React will preserve this state between re-renders. useState returns two things that we can obtain by destructuring: the current state value and a function that lets you update it

    You can call this function setState  function in order to update the state to a new value.

    Consider this example:

    useState là một hàm nhận vào initial state (initialState trong đoạn code trên) như một đối số.

    React sẽ bảo toàn state này giữa các lần render lại. useState trả về hai giá trị mà chúng ta có thể lấy bằng cách  destructuring: giá trị state hiện tại và một hàm cho phép bạn cập nhật giá trị đó.

    Bạn có thể gọi hàm này (thường được đặt tên là setState) để cập nhật state thành một giá trị mới.

    Hãy xem xét ví dụ sau:

    import { useState } from "react"; 
    import "./styles.css";
     
    export default function App() { 
    	const [count, setCount] = useState(0) 
    	return ( 
    		<div className="App"> 
    			<h1> State management 101 </h1> 
    			<button onClick={() => setCount(count + 1)}>
    				Clicked me {count} times
    			</button> 
    		</div>
    	); 
    }
    

    Như chúng ta thấy, cách state được cập nhật trong ví dụ trên khi chúng ta gọi hàm setCount bằng cách nhấn vào button Clicked me ... times.

    Bây giờ, hãy xem một ví dụ thực tiễn hơn. Chúng ta sẽ xem xét các hooks trong component Books – ./src/Books.js:

    import React, { useState } from 'react'
    // array of book objects
    const books = [
    	{
    		title: 'Harry Potter and the Deathly Hallows',
    		price: '5.00',
    		rating: '5.0',
    	},
    	{
    		title: 'Harry Potter and the Goblet of Fire',
    		price: '5.00',
    		rating: '4.8',
    	},
    ]
    
    export function Books() {
    	const [savedBooks, setSavedBooks] = useState([])
    	const [totalPrice, setTotalPrice] = useState(0)
    	
    return (
    	<div>
    		<header>
    			<p> No. of Books: {savedBooks.length} </p>
    			<p> Total price: ${totalPrice} </p>
    		</header>
    		<ul className="Books">
    			{books.map(book => {
    				return (
    					<li className="book" key={book.name}>
    						<header>
    							<h3> {book.title} </h3>
    							<p>Rating: {book.rating} </p>
    							<p> ${book.price} </p>
    						</header>
    						<button> Add </button>
    						<button> Remove </button>
    					</li>
    
    				)
    			})}
    		</ul>
    	</div>
    )}
    

    Sau đó, ở ./src/App.js:

    //./src/App.js
    import { useState } from "react";
    import { Books } from "./Books";
    
    export default function App() {
    	return (
    		<div className="App">
    		<Books />
    	</div>
    	)}
    

    Trong component Books, chúng ta đã nhập hook useState từ React. Sau đó, chúng ta khởi tạo hai state: savedBookstotalPrice.

    Với hooks, chúng ta có thể có nhiều state độc lập chỉ bằng cách gọi hàm useState và cung cấp giá trị khởi tạo cho state đó.

    Hãy xem cách chúng ta có thể cập nhật giá trị của state:

    Để làm điều đó, chúng ta sẽ tạo một hàm để cập nhật state với dữ liệu. Trước tiên, chúng ta sẽ tạo một hàm để thêm một sách từ mảng books vào mảng savedBooks và thiết lập giá.

    import React, { useState } from 'react'
    // array of book objects
    const books = [
    	{
    		title: 'Harry Potter and the Deathly Hallows',
    		price: '5.00',
    		rating: '5.0',
    	},
    	{
    		title: 'Harry Potter and the Goblet of Fire',
    		price: '5.00',
    		rating: '4.8',
    	},
    ]
    
    export function Books() {
    	const [savedBooks, setSavedBooks] = useState([])
    	const [totalPrice, setTotalPrice] = useState(0)
    
    	// functions to set the state
    	const add = (id) => {
    		// set state for savedBooks
    		setSavedBooks([books[0]]);
    		// set state for totalPrice
    		setTotalPrice(books[0].price);
    	};
    
    	// reset state values
    	const reset = () => {
    		// reset values
    		setSavedBooks([]);
    		setTotalPrice(0);
    	};
    	
    	return (
    		<div>
    			<header>
    				<p> No. of Books: {savedBooks.length} </p>
    				<p> Total price: ${totalPrice} </p>
    			</header>
    			<ul className="Books">
    				{books.map(book => {
    					return (
    						<li className="book" key={book.name}>
    							<header>
    								<h3> {book.title} </h3>
    								<p>Rating: {book.rating} </p>
    								<p> ${book.price} </p>
    							</header>
    							<button onClick={add}> Add </button>
    							<button onClick={reset}> Remove </button>
    						</li>
    					)
    				})}
    			</ul>
    		</div>
    )}
    

    Từ đoạn code trên, bạn có thể thấy mình đã tạo hai hàm, add()reset().

    Trong hàm add(), mình sử dụng hàm setSavedBooks() để thiết lập giá trị của savedBooks thành phần tử đầu tiên trong mảng books.

    Hàm add() cũng sử dụng hàm setTotalPrice() để thiết lập giá trị của totalPrice thành giá của phần tử đầu tiên trong mảng books.

    Mình gọi hàm add() khi sự kiện onClick của nút Add được kích hoạt.

    Khi nút được nhấn, savedBooks được gán giá trị của cuốn sách đầu tiên trong danh sách sách ("Harry Potter and the Deathly Hallows") và totalPrice hiện chứa giá của cuốn sách đầu tiên đó (5,00 USD).

    Hàm reset(), khi được nhấn, sẽ thiết lập savedBooks thành một mảng rỗng và totalPrice thành 0.

    Sau khi đã thấy cách thiết lập và cập nhật dữ liệu state với useState, tiếp theo mình sẽ nói về cách sử dụng hook useReducer để cập nhật state dựa trên state hiện tại, vì chúng ta đã sử dụng các giá trị cố định, tĩnh.

    Trong các ứng dụng thực tế, việc thiết lập state mới dựa trên state cũ là cần thiết hơn là ghi đè trực tiếp bằng giá trị gốc. Hãy cùng tìm hiểu cách useReducer hoạt động và cách nó có thể giúp quản lý state hiệu quả hơn.

    2> useReducer hook

    Khác với useState, nơi mà giá trị được thiết lập thay thế hoàn toàn giá trị cũ bằng giá trị mới (ví dụ, khi chúng ta gọi hàm add(), nó thay thế nội dung của state savedBooks bằng dữ liệu mới), useReducer cho phép chúng ta cập nhật state dựa trên state hiện tại. Tương tự như phương thức Array.reduce, hook này được thiết kế để cập nhật state dựa trên trạng thái hiện tại.

    Bắt đầu bằng cách tạo một hàm savedBooksReducer nhận hai tham số: stateaction.

    • state: Là state hiện tại.
    • action: Là một đối tượng chứa hai thuộc tính:
      • book (đối tượng sách)
      • type (loại hành động cần thực hiện trên giá trị của state – "add" hoặc "remove").
    // ./src/Books.js
    const savedBooksReducer = (state, action) => {
    	const { book, type } = action;
    	
    	if (type === "add") return [...state, book];
    	if (type === "remove") {
    		const bookIndex = state.findIndex((x) => x.title === book.title);
    		
    		if (bookIndex < 0) return state;
    		
    		const stateUpdate = [...state];
    		stateUpdate.splice(bookIndex, 1);
    		return stateUpdate;
    	}
    	
    	return state;
    };
    // ...
    export default function Books() {
    // ...
    }
    

    Để tạo một hàm totalPriceReducer, bạn có thể làm theo các bước tương tự như đã làm với savedBooksReducer, nhưng hàm này sẽ chỉ xử lý các hành động liên quan đến giá tổng của các sách.

    // ./src/Books.js
    function totalPriceReducer(state, action) {
    	let { price, type } = action;
    	if (type === "add") return state + price;
    
    	if (state - price < 0) return 0;
    	return state - price;
    }
    // ...
    export default function Books() {
    // ...
    }
    

    Để đảm bảo rằng giá tổng không bao giờ trở thành giá trị âm, bạn cần thêm một điều kiện kiểm tra trong hàm totalPriceReducer để đặt giá tổng về 0 nếu kết quả tính toán nhỏ hơn 0.

    Sử dụng:

    // ./src/Books.js
    export function Books() {
    	const [savedBooks, setSavedBooks] = useReducer(savedBooksReducer, []);
    	
    	const [totalPrice, setTotalPrice] = useReducer(totalPriceReducer, 0);
    
    	const add = (book) => {
    		setSavedBooks({ book, type: "add" });
    		setTotalPrice({ price: book.price, type: "add" });
    	};
    
    	const remove = (book) => {
    		setSavedBooks({ book, type: "remove" });
    		setTotalPrice({ price: book.price, type: "remove" });
    	};
    	
    	return (
    		// ...
    	);
    }
    

    "Mỗi hàm được trả về từ các hàm useReducer tương ứng nhận một đối tượng làm đối số."

    ""
    	const [savedBooks, setSavedBooks] = useReducer(savedBooksReducer, []); 
    	
    	const [totalPrice, setTotalPrice] = useReducer(totalPriceReducer, 0);
    

    Đối với setSavedBooks, nó có thể nhận, chẳng hạn, đối tượng {book, type: 'add'} làm đối số, trong khi setTotalPrice cũng có thể nhận đối tượng {price: book.price, type: 'add'} làm đối số.

    Những đối tượng này sau đó được truyền vào các hàm reducer tương ứng để cập nhật trạng thái. Ví dụ, setSavedBooks truyền đối tượng của nó vào savedBooksReducer.

    Ngoài ra, với việc đã thay đổi các hàm add()remove() để nhận tham số là book, chúng ta giờ đây có thể cung cấp book làm đầu vào cho trình lắng nghe sự kiện onClick của các nút trong giao diện mẫu của chúng ta.

    export function Books() {
    	return (
    	<div>
    		<ul className="Books">
    			{books.map((book) => {
    				return (
    					<li className="book" key={book.name}>
    						<header>
    							<h3> {book.title} </h3>
    							<p>Rating: {book.rating} </p>
    							<p> ${book.price} </p>
    						</header>
    						<button onClick={() => add(book)}> Add </button>
    						<button onClick={() => remove(book)}> Remove </button>
    					</li>
    				);
    			})}
    		</ul>
    	</div>
    )}
    

    Bây giờ, khi chúng ta nhấp vào nút Add hoặc Remove, hàm reducer sẽ thêm dữ liệu mới vào trạng thái trước đó mà không ghi đè lên nó.

    Cho đến nay, chúng ta đã làm việc với state nằm trong một component duy nhất. Trong một ứng dụng thực tế, chúng ta sẽ cần phải truyền data/state giữa nhiều components, từ cha đến con, từ con đến cha, và giữa các thành phần anh chị em (các thành phần có cùng cha).

    Trong phần tiếp theo, chúng ta sẽ xem xét cách quản lý state giữa các thành phần bằng cách sử dụng props."

    3> Props

    ‘Props’ là viết tắt của ‘properties’ và chúng là các đối số được truyền cho các thành phần React. Chúng ta sử dụng props để truyền dữ liệu từ thành phần cha đến thành phần con.

    Để minh họa điều này, chúng ta sẽ book array, các hàm savedBooksReducertotalPriceReducer đến file ./src/App.js của chúng ta.

    // ./src/App.js
    import { Books } from "./Books";
    	const books = [
    		// ...
    	];
    
    	const savedBooksReducer = (state, action) => {
    		// ...
    	};
    
    	function totalPriceReducer(state, action) {
    		// ...
    	}
    
    	export default function App() {
    		return (
    			<div className="App">
    				<Books
    					books={books}
    					savedBooksReducer={savedBooksReducer}
    					totalPriceReducer={totalPriceReducer}
    				/>
    			</div>
    	)}
    

    Như bạn thấy từ mã trên, để truyền dữ liệu vào components <Books>, chúng ta sử dụng các liên kết thành phần JSX và truyền dữ liệu.

    Để truy cập dữ liệu này từ bên trong components, chúng ta phân tách props trong định nghĩa hàm Books của chúng ta.

    import React, { useReducer } from "react";
    
    export function Books({ books, savedBooksReducer, totalPriceReducer }) {
    	// ...
    }
    

    Khi ứng dụng trở nên phức tạp hơn và dữ liệu cần được truyền từ các components con trở lại components cha, giữa các components anh chị em, hoặc thậm chí global trong ứng dụng, việc chỉ dựa vào props có thể không còn hiệu quả nữa.

    Tiếp theo, chúng ta sẽ tìm hiểu về Context API, công cụ giúp chúng ta chia sẻ state một cách hiệu quả hơn.

    5.2 Context API

    Context API cung cấp một cách để di chuyển dữ liệu lên và xuống cây components của ứng dụng mà không cần phải truyền props thủ công qua nhiều cấp components. Bạn có thể cấu hình một global state cho một cây các components trong React bằng cách sử dụng Context. Sau khi thực hiện, state này có thể được truy cập từ bất kỳ components nào trong cây mà không cần phải truyền qua các thành phần trung gian.

    Hãy tiếp tục tái cấu trúc ứng dụng của chúng ta bằng cách tạo một components mới, SavedBooks, sẽ chứa tổng số sách đã lưu và tổng giá. Components Books vẫn sẽ chứa danh sách các quyển sách.

    Trước khi làm điều đó, hãy thiết lập global state trong ứng dụng của chúng ta bằng Context API.

    1> Khởi tạo:

    Bạn hãy tạo một file mới ./src/modules/BooksContext.js dành riêng cho context của chúng ta, sau đó nhập createContext và sử dụng nó để tạo BooksContext.

    // ./src/modules/BooksContext.js 
    import React, {createContext} from "react" 
    
    export const BooksContext = createContext()
    

    Tiếp theo, chúng ta tạo một context provider.

    2> Tạo context provider:

    Một đối tượng context đi kèm với một component Provider cho phép các component được bọc bên trong nó theo dõi các thay đổi của context.

    Chúng ta sẽ lại di chuyển mảng sách và các hàm savedBooksReducer vào file ./src/modules/BooksContext.js và khởi tạo state trong một hàm mới BooksProvider bằng cách sử dụng các hook useStateuseReducer.

    import React, { useContext, useReducer, useState } from "react";
    
    const bookList = [
    // ...
    ];
    
    const savedBooksReducer = (state, action) => {
    // ...
    };
    
    export const BooksContext = createContext();
      
    export const BooksProvider = (props) => {
    	const [books, setBooks] = useState(bookList)
    	const [savedBooks, setSavedBooks] = useReducer(savedBooksReducer, [])
    	
    	return <BooksContext.Provider> {props.children} </BooksContext.Provider>
    }
    

    Trong đoạn code trên, bạn có thể thấy rằng chúng ta đang trả về component Provider <BooksContext.Provider> từ BooksProvider.

    props.children sẽ cho phép chúng ta render các tcomponent được lồng ghép bên trong provider.

    3> Truy cập state

    Các component là con của Provider có thể truy cập vào thuộc tính value mà component <BooksContext.Provider> chấp nhận.

    Tất cả các state của chúng ta phải được đưa vào thuộc tính value này trong tệp context của chúng ta như sau:

    // ./src/modules/BooksContext.js ... 
    return ( 
    	<BooksContext.Provider
    		value={{ bookList, setBookList, savedBooks, setSavedBooks, }}>
    			 {props.children}
    	</BooksContext.Provider> ); ...
    

    Để component này có thể được truy cập từ phần còn lại của ứng dụng của chúng ta, chúng ta sẽ phải tái cấu trúc file ./src/App.js để sử dụng và bọc ứng dụng bằng component BooksProvider.

    // ./src/App.js
    import { Books } from "./Books";
    import { BooksProvider } from "./modules/BooksContext";
    
    export default function App() {
    	return (
    		<BooksProvider>
    			<div className="App">
    				<Books />
    			</div>
    		</BooksProvider>
    )}
    

    Để state được truyền đến component provider trong các component của chúng ta, chúng ta cần nhập BooksContext và sử dụng hook useContext để sử dụng nó.

    Dưới đây là cách chúng ta có thể làm điều đó trong component Books của chúng ta:

    import React, { useContext } from "react";
    import { BooksContext } from "./modules/BooksContext";
    
    	export function Books() {
    	const { books, setSavedBooks } = useContext(BooksContext);
    	
    	const add = (book) => {
    		setSavedBooks({ book, type: "add" });
    	};
    
    	const remove = (book) => {
    		setSavedBooks({ book, type: "remove" });
    	};
    
    return (
    	<div>
    		<ul className="Books">
    			{books.map((book) => {
    				return (
    					<li className="book" key={book.name}>
    						<header>
    							<h3> {book.title} </h3>
    							<p>Rating: {book.rating} </p>
    							<p> ${book.price} </p>
    						</header>
    					
    						<button onClick={() => add(book)}> Add </button>
    						<button onClick={() => remove(book)}> Remove </button>
    					</li>
    				);
    			})}
    		</ul>
    	</div>
    )}
    

    Dựa vào đoạn code trên, chúng ta có thể truy cập state bookssetSavedBooks từ BooksContext bằng cách sử dụng useContext.

    Bây giờ mà chúng ta đã thiết lập global state với context, hãy tạo component SavedBooks của chúng ta:

    import React, { useContext } from "react";
    import { BooksContext } from "./modules/BooksContext";
    
    export function SavedBooks() {
    	const { savedBooks } = useContext(BooksContext);
    	
    	const getTotalPrice = (savedBooks) => {
    		const totalPrice = savedBooks.reduce((totalCost, item) => totalCost + item.price,0);
    		
    		return totalPrice;
    	};
    
    	return (
    		<header>
    			<p> No. of Books: {savedBooks.length} </p>
    			<p> Total price: ${getTotalPrice(savedBooks)} </p>
    		</header>
    	);
    }
    

    Cuối cùng, sau khi chúng ta đã tách component Books và tạo component SavedBooks, hãy thêm nó vào ./src/App.js:

    import { Books } from "./Books";
    import { BooksProvider } from "./modules/BooksContext";
    import { SavedBooks } from "./SavedBooks";
    
    export default function App() {
    	return (
    		<BooksProvider>
    			<div className="App">
    				<SavedBooks />
    				<Books />
    			</div>
    		</BooksProvider>
    )}
    

    Bây giờ, bạn có thể thấy rằng chúng ta có thể truyền dữ liệu giữa các component anh chị em bằng cách sử dụng Context API

    6. Tổng Kết

    Cho đến nay, chúng ta đã tìm hiểu các khái niệm cơ bản về quản lý state trong một ứng dụng React bằng cách sử dụng các React Hooks như useStateuseReducer.

    Chúng ta cũng đã tìm hiểu cách nâng cao về quản lý state component bằng cách thiết lập global state bằng Context API, một tính năng có sẵn trong React.

    Với điều này, chúng ta có thể xây dựng các ứng dụng nhỏ đến vừa mà không cần cài đặt và phụ thuộc vào các thư viện quản lý trạng thái bên thứ ba.

    Cảm ơn mọi người đã dành thời gian xem qua bài viết này, se yaaa !

    React

    React Context

    Categories:

    Frontend ,ReactJS

    logo

    Hoàng Sơn

    Cảm ơn bạn đã dành thời gian đọc qua bài viết trên của mình, nếu có bất kỳ câu hỏi gì, thì cứ nhắn tin cho mình nhé. Hi vọng mình đã giúp ích cho bạn 'một phần nào đấy'.

    There are 0 comments on this post

    Comment

    Your email address will not be published. Required fields are marked * are required.