Bài 20.3: Testing hooks và context

Bài 20.3: Testing hooks và context
Chào mừng bạn đến với bài viết chuyên sâu về kiểm thử (testing) trong React, tập trung vào hai khía cạnh cực kỳ quan trọng và thường gặp trong các ứng dụng hiện đại: React Hooks và Context API. Việc kiểm thử các thành phần này giúp bạn xây dựng ứng dụng mạnh mẽ hơn, ít lỗi hơn và dễ dàng bảo trì hơn theo thời gian.
Chúng ta sẽ sử dụng React Testing Library (RTL), thư viện kiểm thử được khuyến khích bởi đội ngũ React, với triết lý tập trung vào cách người dùng tương tác với ứng dụng của bạn.
Tại sao phải kiểm thử Hooks và Context?
- Hooks: Hooks là các hàm cho phép bạn "móc nối" vào các tính năng của React state và lifecycle từ các component hàm. Chúng thường chứa đựng logic xử lý state, side effects (như gọi API, quản lý subscriptions), hoặc encapsulate các behavior phức tạp. Kiểm thử hooks độc lập giúp bạn đảm bảo logic đó hoạt động chính xác mà không cần render toàn bộ component chứa nó. Điều này giúp isolate lỗi và làm unit test ngắn gọn hơn.
- Context API: Context là một cách để chia sẻ dữ liệu (state, hàm,...) qua cây component mà không cần truyền props xuống thủ công qua mọi cấp. Nó là trái tim của việc quản lý state global đơn giản hoặc inject dependencies (như theme, user info). Việc kiểm thử Context đảm bảo rằng các component tiêu thụ (consume) dữ liệu từ Context một cách đúng đắn và các component cung cấp (provide) dữ liệu hoạt động như mong đợi.
Việc kết hợp kiểm thử hooks và context mang lại sự tự tin rằng các phần cốt lõi, quản lý state và logic chia sẻ của ứng dụng bạn đang hoạt động chính xác.
Kiểm thử React Hooks với renderHook
React Testing Library cung cấp một tiện ích đặc biệt cho việc kiểm thử hooks: @testing-library/react-hooks
. Thư viện này cung cấp hàm renderHook
, cho phép bạn "render" một hook trong một môi trường React thử nghiệm mà không cần một component DOM thực sự.
Cú pháp cơ bản:
import { renderHook, act } from '@testing-library/react-hooks';
// ... hook cần test ...
test('tên bài kiểm thử hook', () => {
// Render hook
const { result, rerender, unmount } = renderHook(() => tenHook(propsBanDau));
// result.current chứa giá trị trả về hiện tại của hook
// ... kiểm tra state ban đầu ...
// act() được sử dụng để wrap các hành động thay đổi state/gây side effects
act(() => {
// Gọi hàm từ hook để thay đổi state
result.current[1](giaTriMoi); // Ví dụ cho useState [state, setState]
});
// ... kiểm tra state sau khi thay đổi ...
// rerender(newProps) để test khi props thay đổi
// unmount() để test cleanup effects (useEffect)
});
Hãy xem xét một vài ví dụ cụ thể.
Ví dụ 1: Kiểm thử hook quản lý state đơn giản (useCounter
)
Giả sử bạn có một hook đơn giản để quản lý bộ đếm:
// hooks/useCounter.js
import { useState, useCallback } from 'react';
const useCounter = (initialValue = 0) => {
const [count, setCount] = useState(initialValue);
const increment = useCallback(() => {
setCount(prevCount => prevCount + 1);
}, []);
const decrement = useCallback(() => {
setCount(prevCount => prevCount - 1);
}, []);
const reset = useCallback(() => {
setCount(initialValue);
}, [initialValue]); // initialValue là dependency
return { count, increment, decrement, reset };
};
export default useCounter;
Đây là cách bạn kiểm thử nó:
// hooks/useCounter.test.js
import { renderHook, act } from '@testing-library/react-hooks';
import useCounter from './useCounter';
test('useCounter should initialize with default or provided value', () => {
// Test default value
const { result: resultDefault } = renderHook(() => useCounter());
expect(resultDefault.current.count).toBe(0);
// Test provided initial value
const { result: resultInitial } = renderHook(() => useCounter(10));
expect(resultInitial.current.count).toBe(10);
});
test('useCounter should increment the count', () => {
const { result } = renderHook(() => useCounter(5)); // Start from 5
expect(result.current.count).toBe(5);
// Use act() for state updates
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(6); // Check after increment
act(() => {
result.current.increment();
result.current.increment();
});
expect(result.current.count).toBe(8); // Check after multiple increments
});
test('useCounter should decrement the count', () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(4);
act(() => {
result.current.decrement();
result.current.decrement();
});
expect(result.current.count).toBe(2);
});
test('useCounter should reset the count to initial value', () => {
const { result } = renderHook(() => useCounter(10));
act(() => {
result.current.increment();
result.current.increment();
});
expect(result.current.count).toBe(12); // Change the state
act(() => {
result.current.reset();
});
expect(result.current.count).toBe(10); // Reset to initial (10)
});
test('useCounter should reset to new initial value if props change', () => {
const { result, rerender } = renderHook(({ initialValue }) => useCounter(initialValue), {
initialProps: { initialValue: 0 },
});
expect(result.current.count).toBe(0);
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
// Rerender with new initialValue
rerender({ initialValue: 100 });
// The counter state is preserved by default
expect(result.current.count).toBe(1);
// Reset should now use the new initialValue
act(() => {
result.current.reset();
});
expect(result.current.count).toBe(100);
});
Giải thích code test:
- Chúng ta import
renderHook
vàact
từ@testing-library/react-hooks
. renderHook(() => useCounter())
: Gọi hook cần test bên trong hàm callback.result.current
truy cập vào giá trị trả về của hook ({ count, increment, decrement, reset }
).expect(result.current.count).toBe(...)
: Kiểm tra giá trị state hiện tại.act(() => { ... })
: Mọi thao tác gây ra thay đổi state hoặc side effects trong React (như gọiincrement()
) phải được bao bọc bởiact()
. Điều này đảm bảo React xử lý xong các bản cập nhật trước khi bạn thực hiện các assertion.rerender({ initialValue: 100 })
: Minh họa cách test hook khi props đầu vào của nó thay đổi.useCounter
nhậninitialValue
như một "props". Khirerender
được gọi với props mới, hook sẽ được chạy lại với props đó. Lưu ý rằng state không tự động reset khi props thay đổi, trừ khi logic hook của bạn xử lý điều đó (như hàmreset
của chúng ta dựa vàoinitialValue
).
Ví dụ 2: Kiểm thử Hook với Side Effects và Async Logic (useFetch
)
Kiểm thử hooks có useEffect
hoặc chứa logic bất đồng bộ (async/await) đòi hỏi thêm một chút công sức, thường sử dụng waitFor
.
Giả sử bạn có một hook đơn giản để fetch dữ liệu:
// hooks/useFetch.js
import { useState, useEffect } from 'react';
const useFetch = (url) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
setError(null); // Reset error on new fetch
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
if (url) { // Only fetch if url is provided
fetchData();
} else {
setLoading(false); // If no URL, not loading
setData(null);
setError(null);
}
}, [url]); // Re-run effect if URL changes
return { data, loading, error };
};
export default useFetch;
Để kiểm thử hook này, bạn cần mock hàm fetch
toàn cục hoặc sử dụng các thư viện mock network như msw
(Mock Service Worker). Với ví dụ đơn giản, chúng ta có thể mock fetch
trực tiếp.
// hooks/useFetch.test.js
import { renderHook, waitFor } from '@testing-library/react-hooks';
import useFetch from './useFetch';
// Mock the global fetch function
const mockFetch = (data, ok = true, status = 200) =>
jest.fn(() =>
Promise.resolve({
ok,
status,
json: () => Promise.resolve(data),
})
);
const mockErrorFetch = (error, ok = false, status = 500) =>
jest.fn(() =>
Promise.resolve({
ok,
status,
json: () => Promise.reject(error), // Or handle error body if needed
})
);
describe('useFetch', () => {
beforeEach(() => {
// Reset mocks before each test
global.fetch = jest.fn();
});
afterEach(() => {
// Clean up mocks
jest.resetAllMocks();
});
test('should fetch data successfully', async () => {
const dummyData = { id: 1, name: 'Test Item' };
global.fetch = mockFetch(dummyData); // Set up the mock
const url = 'https://example.com/api/items/1';
const { result, rerender } = renderHook(() => useFetch(url));
// Initial state
expect(result.current.loading).toBe(true);
expect(result.current.data).toBeNull();
expect(result.current.error).toBeNull();
// Wait for the async operation (fetch) to complete and state to update
await waitFor(() => {
expect(result.current.loading).toBe(false);
expect(result.current.data).toEqual(dummyData);
expect(result.current.error).toBeNull();
});
// Verify fetch was called with the correct URL
expect(global.fetch).toHaveBeenCalledWith(url);
expect(global.fetch).toHaveBeenCalledTimes(1);
// Test fetching with a new URL
const dummyData2 = { id: 2, name: 'Another Item' };
global.fetch = mockFetch(dummyData2); // Set up mock for the new fetch
const newUrl = 'https://example.com/api/items/2';
// Rerender hook with new URL, which should trigger new effect
rerender(newUrl);
// State should go back to loading initially (depending on hook logic)
expect(result.current.loading).toBe(true);
expect(result.current.data).toBeNull(); // Data might reset or not, depends on hook impl.
// Wait for the second fetch to complete
await waitFor(() => {
expect(result.current.loading).toBe(false);
expect(result.current.data).toEqual(dummyData2);
expect(result.current.error).toBeNull();
});
// Verify fetch was called again with the new URL
expect(global.fetch).toHaveBeenCalledWith(newUrl);
expect(global.fetch).toHaveBeenCalledTimes(2); // Total calls
});
test('should handle fetch error', async () => {
const fetchError = new Error('Failed to fetch');
global.fetch = mockErrorFetch(fetchError); // Set up error mock
const url = 'https://example.com/api/error';
const { result } = renderHook(() => useFetch(url));
// Initial state
expect(result.current.loading).toBe(true);
expect(result.current.data).toBeNull();
expect(result.current.error).toBeNull();
// Wait for the async operation to complete (and fail)
await waitFor(() => {
expect(result.current.loading).toBe(false);
expect(result.current.data).toBeNull();
expect(result.current.error).toEqual(expect.any(Error)); // Check if error is an Error object
// Or specific error message if needed: expect(result.current.error.message).toBe('Failed to fetch');
});
expect(global.fetch).toHaveBeenCalledWith(url);
expect(global.fetch).toHaveBeenCalledTimes(1);
});
test('should not fetch if url is null or undefined', () => {
const { result } = renderHook(() => useFetch(null)); // Test with null URL
expect(global.fetch).not.toHaveBeenCalled(); // Fetch should not be called
expect(result.current.loading).toBe(false); // Should not be loading
expect(result.current.data).toBeNull();
expect(result.current.error).toBeNull();
});
});
Giải thích code test:
- Chúng ta mock hàm
fetch
toàn cục bằngjest.fn()
.mockFetch
vàmockErrorFetch
là các hàm helper để tạo ra các mock response/reject. await waitFor(() => { ... })
: Đây là hàm quan trọng nhất khi test logic bất đồng bộ. Nó đợi cho đến khi hàm callback bên trong không còn ném lỗi nữa (hoặc timeout). Chúng ta sử dụng nó để chờ đợifetch
hoàn thành và hook cập nhật state (loading -> false, data/error được set).- Kiểm tra state ban đầu (
loading: true
,data/error: null
). - Kiểm tra state sau khi
waitFor
hoàn thành (dữ liệu đã có hoặc lỗi đã được set,loading: false
). - Sử dụng
expect(global.fetch).toHaveBeenCalledWith(...)
vàtoHaveBeenCalledTimes(...)
để xác minh rằng hàmfetch
đã được gọi đúng số lần với đúng URL. - Bài test thứ hai minh họa cách xử lý trường hợp lỗi khi fetch.
- Bài test thứ ba kiểm tra điều kiện không gọi fetch nếu URL không hợp lệ.
rerender(newUrl)
trong bài test đầu tiên cho thấy cách kiểm thử khi dependency củauseEffect
thay đổi.
Kiểm thử các hooks phức tạp hơn (với useReducer
, useCallback
, useMemo
, tương tác DOM với useRef
và useEffect
) sẽ tuân theo các nguyên tắc tương tự: renderHook, sử dụng act cho các hành động gây thay đổi state/effects, và sử dụng waitFor cho logic bất đồng bộ.
Kiểm thử Context API
Kiểm thử Context API thường liên quan đến việc kiểm thử các component hoặc hooks sử dụng Context. RTL tập trung vào kiểm thử cách người dùng tương tác với giao diện, do đó, khi kiểm thử Context, chúng ta thường render các component bao bọc bởi Provider cần thiết.
Ví dụ 3: Kiểm thử Component Tiêu Thụ Context (Consumer)
Giả sử bạn có UserContext
và UserProvider
như đã giới thiệu ở phần trên:
// UserContext.js (Lặp lại để tiện theo dõi)
import React, { createContext, useContext, useState } from 'react';
const UserContext = createContext({ name: 'Guest', setName: () => {} });
const UserProvider = ({ children }) => {
const [name, setName] = useState('Default User'); // Giá trị mặc định thực tế
return (
<UserContext.Provider value={{ name, setName }}>
{children}
</UserContext.Provider>
);
};
const useUser = () => useContext(UserContext);
export { UserContext, UserProvider, useUser };
// MyComponent.js (Component tiêu thụ Context)
import React from 'react';
import { useUser } from './UserContext';
const MyComponent = () => {
const { name, setName } = useUser(); // Lấy name và setName từ context
return (
<div>
<h1>Xin chào, {name}!</h1>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Nhập tên của bạn"
/>
</div>
);
};
export default MyComponent;
Để test MyComponent
, nó cần được đặt bên trong một UserProvider
.
// MyComponent.test.js
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import MyComponent from './MyComponent';
import { UserProvider } from './UserContext'; // Import the real provider
test('MyComponent displays default user name from Provider and updates on input', async () => {
render(
// Wrap the component with the necessary Provider
<UserProvider>
<MyComponent />
</UserProvider>
);
// Check if the default name from Provider is displayed initially
expect(screen.getByText('Xin chào, Default User!')).toBeInTheDocument();
const inputElement = screen.getByPlaceholderText('Nhập tên của bạn');
expect(inputElement).toHaveValue('Default User'); // Input reflects context state
// Simulate user typing into the input
const newName = 'Alice';
fireEvent.change(inputElement, { target: { value: newName } });
// Wait for state update and re-render (fireEvent is usually wrapped in act internally by RTL)
// We can directly check the updated state shown in the UI
expect(screen.getByText(`Xin chào, ${newName}!`)).toBeInTheDocument();
expect(inputElement).toHaveValue(newName);
});
test('MyComponent uses default context value if no Provider is present (not recommended)', () => {
// Test without wrapping in Provider - relies on the default value in createContext
// This test is mainly for understanding, typically you'd ensure Provider is always used
render(<MyComponent />);
// Check if the default value from createContext is used
// Note: The default value in createContext is used *only* when the component
// is rendered *outside* of a matching Provider.
expect(screen.getByText('Xin chào, Guest!')).toBeInTheDocument(); // 'Guest' is the default in createContext
const inputElement = screen.getByPlaceholderText('Nhập tên của bạn');
expect(inputElement).toHaveValue('Guest'); // Input reflects context state
// Cannot test state updates easily here as the default context value is static.
});
// Alternative: Mocking Context Value
test('MyComponent displays mocked user name when using a mocked Provider wrapper', () => {
// This is useful if the real Provider is complex or has side effects.
// We create a simple mock wrapper that provides specific context values.
const MockUserProvider = ({ children }) => {
const mockValue = { name: 'Mock User', setName: jest.fn() };
return (
<UserContext.Provider value={mockValue}>
{children}
</UserContext.Provider>
);
};
render(
<MockUserProvider>
<MyComponent />
</MockUserProvider>
);
expect(screen.getByText('Xin chào, Mock User!')).toBeInTheDocument();
const inputElement = screen.getByPlaceholderText('Nhập tên của bạn');
expect(inputElement).toHaveValue('Mock User');
// We can even test if the mocked setName function was called
const setNameMock = screen.getByText('Xin chào, Mock User!').closest('div').querySelector('input')._wrapperState.initialValue.setName; // This is a bit hacky way to get the mock function in testing. A better way is to pass the mock value directly in render.
// Let's use a cleaner approach by passing the mock value to the render context directly if needed,
// or simply testing the output based on the mocked value. Testing the mock itself is less common
// unless you're testing the Provider's logic. The main goal here is testing the *consumer*.
// Cleaner way to test with mocked context value:
render(
<UserContext.Provider value={{ name: 'Another Mock', setName: jest.fn() }}>
<MyComponent />
</UserContext.Provider>
);
expect(screen.getByText('Xin chào, Another Mock!')).toBeInTheDocument();
});
Giải thích code test:
- Import
render
,screen
,fireEvent
từ@testing-library/react
. - Import
MyComponent
vàUserProvider
. render(<UserProvider><MyComponent /></UserProvider>)
: RenderMyComponent
bên trongUserProvider
. Đây là cách RTL kiểm thử component sử dụng context: đảm bảo môi trường runtime giống với khi nó chạy trong ứng dụng thật.screen.getByText(...)
vàexpect(...).toBeInTheDocument()
: Kiểm tra xem component có hiển thị nội dung đúng dựa trên giá trị context hay không.fireEvent.change(...)
: Mô phỏng hành động của người dùng (gõ vào input). RTL đảm bảo các sự kiện này được wrap trongact
.- Kiểm tra lại UI sau khi sự kiện xảy ra để xác nhận context đã được cập nhật và component re-render đúng.
- Bài test thứ hai cho thấy điều gì xảy ra nếu component không được wrap bởi Provider (nó sẽ sử dụng giá trị
defaultValue
trongcreateContext
). Thường bạn sẽ muốn viết test để đảm bảo component thông báo lỗi hoặc hiển thị trạng thái loading/error nếu Provider bị thiếu, thay vì dựa vàodefaultValue
. - Bài test thứ ba minh họa cách tạo một mock Provider đơn giản hoặc sử dụng trực tiếp
Context.Provider
với mộtvalue
được mock. Điều này hữu ích khi Provider thật phức tạp hoặc bạn muốn kiểm soát chính xác giá trị context trong test case cụ thể.
Ví dụ 4: Kiểm thử Hook Sử Dụng Context
Như đã thấy ở trên, chúng ta có hook useUser
đơn giản chỉ là wrapper quanh useContext(UserContext)
. Để kiểm thử hook này, chúng ta cần sử dụng renderHook
nhưng cung cấp Context cho nó bằng tùy chọn wrapper
.
// hooks/useUser.test.js (Kiểm thử hook useUser)
import { renderHook, act } from '@testing-library/react-hooks';
import { useUser, UserProvider, UserContext } from './UserContext'; // Import hook VÀ Provider/Context
test('useUser hook returns context value when wrapped with Provider', () => {
// Sử dụng UserProvider thực tế làm wrapper cho renderHook
const { result } = renderHook(() => useUser(), {
wrapper: UserProvider, // Chỉ định Provider làm wrapper
});
// Kiểm tra giá trị ban đầu được cung cấp bởi UserProvider
const { name, setName } = result.current;
expect(name).toBe('Default User');
expect(typeof setName).toBe('function');
});
test('useUser hook throws error when used outside of Provider', () => {
// Jest will capture console.error for expected React warnings/errors
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
// renderHook does not automatically wrap in act if there's an error
// We expect the hook to throw when useContext cannot find a Provider
expect(() => {
renderHook(() => useUser()); // Render without wrapper
}).toThrow(/useContext can only be called inside of the top level of a React function component or custom Hook./);
// The error message might vary slightly depending on React/Jest version.
// It might also be a different error if you used createContext(null) initially.
consoleErrorSpy.mockRestore(); // Restore console.error
});
test('useUser hook returns mocked context value when using a mocked Provider wrapper', () => {
const mockValue = { name: 'Mocked Name', setName: jest.fn() };
// Create a simple wrapper using the Context.Provider directly
const MockWrapper = ({ children }) => (
<UserContext.Provider value={mockValue}>
{children}
</UserContext.Provider>
);
const { result } = renderHook(() => useUser(), {
wrapper: MockWrapper, // Sử dụng wrapper mock
});
// Kiểm tra giá trị được trả về từ mock value
const { name, setName } = result.current;
expect(name).toBe('Mocked Name');
expect(setName).toBe(mockValue.setName); // Kiểm tra xem có phải cùng function mock không
// Optional: Test calling the function returned by the hook
act(() => {
setName('New Name'); // Call the mocked function
});
expect(mockValue.setName).toHaveBeenCalledWith('New Name'); // Verify the mock function was called
});
Giải thích code test:
renderHook(() => useUser(), { wrapper: UserProvider })
: Sử dụng tùy chọnwrapper
củarenderHook
để bao bọc việc render hookuseUser
bằngUserProvider
. Điều này giả lập môi trường mà hook sẽ chạy trong ứng dụng thật.- Bài test đầu tiên kiểm tra xem hook có lấy được giá trị mặc định từ
UserProvider
hay không. - Bài test thứ hai kiểm tra điều gì xảy ra nếu
useUser
được gọi không có Provider (nó sẽ ném lỗi). Đây là một test quan trọng để đảm bảo hook của bạn được sử dụng đúng cách hoặc để kiểm thử xử lý lỗi nếu Provider bị thiếu. - Bài test thứ ba cho thấy cách tạo một
wrapper
đơn giản sử dụng trực tiếpUserContext.Provider
với mộtvalue
được mock. Điều này cho phép bạn kiểm soát chính xác giá trị context mà hook nhận được trong quá trình test. Bạn có thể mock cả các hàm trong context và kiểm tra xem chúng có được gọi hay không.
Ví dụ 5: Kiểm thử Provider Component
Đôi khi, bạn cũng muốn kiểm thử chính Provider component, đặc biệt nếu nó có logic phức tạp hơn, ví dụ như quản lý state nội bộ hoặc tương tác với bên ngoài.
// UserContext.test.js (Kiểm thử Provider)
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { UserProvider, UserContext } from './UserContext'; // Import Provider VÀ Context
test('UserProvider provides initial value and updates it via the provided function', async () => {
// Để kiểm thử Provider, chúng ta cần một cách để "nhìn vào" giá trị context được cung cấp.
// Một cách là render một component con đơn giản chỉ hiển thị giá trị context.
const ConsumerComponent = () => {
const { name, setName } = React.useContext(UserContext);
return (
<div>
<span data-testid="user-name">{name}</span>
<button onClick={() => setName('Updated User')}>Update Name</button>
</div>
);
};
render(
<UserProvider>
<ConsumerComponent />
</UserProvider>
);
// Check initial state provided by UserProvider
expect(screen.getByTestId('user-name')).toHaveTextContent('Default User');
const updateButton = screen.getByText('Update Name');
// Simulate user clicking the button to update state via the context function
fireEvent.click(updateButton);
// Wait for state update and re-render
await screen.findByText('Updated User'); // Wait until 'Updated User' appears
// Check the updated state reflected in the UI
expect(screen.getByTestId('user-name')).toHaveTextContent('Updated User');
});
Giải thích code test:
- Chúng ta cần một
ConsumerComponent
nhỏ gọn bên trong bài test để có thể truy cập và hiển thị giá trị từ Context Provider. Component này hoạt động như một cửa sổ để chúng ta quan sát state bên trong Provider. - Render
ConsumerComponent
được wrap bởiUserProvider
. - Kiểm tra nội dung ban đầu của
ConsumerComponent
để xác nhận giá trị mặc định được cung cấp bởiUserProvider
. - Sử dụng
fireEvent
để mô phỏng tương tác gọi hàmsetName
được cung cấp qua context. - Sử dụng
await screen.findByText(...)
hoặcwaitFor
để đợi cho đến khi UI cập nhật phản ánh sự thay đổi state bên trong Provider do hàmsetName
gây ra. - Kiểm tra lại nội dung UI để xác nhận Provider đã cập nhật state nội bộ và cung cấp giá trị mới qua context.
Comments