When building a React application, separating the logic and state management from the UI can make your code easier to manage, test, and reuse. This is where the view model pattern comes in handy. By using a custom hook as a view model, you can keep your components focused on displaying the UI while the hook handles all the complex logic and state. Let's break down how to do this with a simple example.
Creating custom hook
A custom hook is a JavaScript function that uses React hooks like useState and useEffect to manage state and logic. In this example, we’ll create a custom hook to manage a list of items. The hook will handle fetching items from an API, adding new items, and removing items.
Testing a custom hook in React, like the view model we created, involves using a testing library like @testing-library/react-hooks along with a testing framework like Jest. This allows you to isolate the logic in the hook and verify its behavior.
Here’s how you can write unit tests for the useItemsViewModel custom hook.
Here's how you can write tests for the custom hook:
// useItemsViewModel.test.js
import { renderHook, act } from '@testing-library/react-hooks';
import useItemsViewModel from './useItemsViewModel';
// Mock fetch API
global.fetch = jest.fn();
const mockItems = [{ id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }];
beforeEach(() => {
fetch.mockClear();
});
test('should fetch items successfully', async () => {
fetch.mockResolvedValueOnce({
json: async () => mockItems,
});
const { result, waitForNextUpdate } = renderHook(() => useItemsViewModel());
// Initially loading should be true
expect(result.current.loading).toBe(true);
// Wait for the fetch to complete
await waitForNextUpdate();
// Verify the hook state after fetching items
expect(result.current.items).toEqual(mockItems);
expect(result.current.loading).toBe(false);
expect(result.current.error).toBe(null);
});
test('should handle fetch error', async () => {
fetch.mockRejectedValueOnce(new Error('Failed to fetch'));
const { result, waitForNextUpdate } = renderHook(() => useItemsViewModel());
// Initially loading should be true
expect(result.current.loading).toBe(true);
// Wait for the fetch to complete
await waitForNextUpdate();
// Verify the hook state after the fetch error
expect(result.current.items).toEqual([]);
expect(result.current.loading).toBe(false);
expect(result.current.error).toBe('Failed to fetch');
});
test('should add a new item', async () => {
fetch.mockResolvedValueOnce({
json: async () => mockItems,
});
const { result, waitForNextUpdate } = renderHook(() => useItemsViewModel());
// Wait for the initial fetch to complete
await waitForNextUpdate();
const newItem = { id: 3, name: 'Item 3' };
fetch.mockResolvedValueOnce({
json: async () => newItem,
});
await act(async () => {
await result.current.addItem(newItem);
});
// Verify the hook state after adding an item
expect(result.current.items).toEqual([...mockItems, newItem]);
});
test('should remove an item', async () => {
fetch.mockResolvedValueOnce({
json: async () => mockItems,
});
const { result, waitForNextUpdate } = renderHook(() => useItemsViewModel());
// Wait for the initial fetch to complete
await waitForNextUpdate();
fetch.mockResolvedValueOnce({
json: async () => ({}),
});
await act(async () => {
await result.current.removeItem(1);
});
// Verify the hook state after removing an item
expect(result.current.items).toEqual(mockItems.filter(item => item.id !== 1));
});
By using a custom hook as a view model, you encapsulate all the state management and business logic within the hook. This makes your components focused on rendering the UI, leading to cleaner, more maintainable, and reusable code. The custom hook handles all the complex logic, allowing the component to remain simple and focused on what it does best—displaying the UI.
When building a React application, separating the logic and state management from the UI can make your code easier to manage, test, and reuse. This is where the view model pattern comes in handy. By using a custom hook as a view model, you can keep your components focused on displaying the UI while the hook handles all the complex logic and state. Let's break down how to do this with a simple example.
Creating custom hook
A custom hook is a JavaScript function that uses React hooks like useState and useEffect to manage state and logic. In this example, we’ll create a custom hook to manage a list of items. The hook will handle fetching items from an API, adding new items, and removing items.
Here’s how you can create the custom hook:
In this hook:
Using the Custom Hook in a Component
Now, let's create a component that uses this custom hook to display the list of items and allows users to add or remove items.
In this component:
Integrating into Your App
Finally, integrate the component into your main application
Testing view model
Testing a custom hook in React, like the view model we created, involves using a testing library like
@testing-library/react-hooks
along with a testing framework like Jest. This allows you to isolate the logic in the hook and verify its behavior.Here’s how you can write unit tests for the useItemsViewModel custom hook.
Here's how you can write tests for the custom hook:
By using a custom hook as a view model, you encapsulate all the state management and business logic within the hook. This makes your components focused on rendering the UI, leading to cleaner, more maintainable, and reusable code. The custom hook handles all the complex logic, allowing the component to remain simple and focused on what it does best—displaying the UI.