Skip to content

Commit cfdbe42

Browse files
committed
Compose Axios recipe
1 parent fd2cba8 commit cfdbe42

File tree

7 files changed

+264
-14
lines changed

7 files changed

+264
-14
lines changed

examples/cookbook/app/network-requests/PhoneBook.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React, { useEffect, useState } from 'react';
2+
import { Text } from 'react-native';
23
import { User } from './types';
34
import ContactsList from './components/ContactsList';
45
import FavoritesList from './components/FavoritesList';
@@ -8,6 +9,8 @@ import getAllFavorites from './api/getAllFavorites';
89
export default () => {
910
const [usersData, setUsersData] = useState<User[]>([]);
1011
const [favoritesData, setFavoritesData] = useState<User[]>([]);
12+
const [error, setError] = useState<string | null>(null);
13+
1114
useEffect(() => {
1215
const _getAllContacts = async () => {
1316
const _data = await getAllContacts();
@@ -19,12 +22,20 @@ export default () => {
1922
};
2023

2124
const run = async () => {
22-
await Promise.all([_getAllContacts(), _getAllFavorites()]);
25+
try {
26+
await Promise.all([_getAllContacts(), _getAllFavorites()]);
27+
} catch (e) {
28+
setError(e.message);
29+
}
2330
};
2431

2532
void run();
2633
}, []);
2734

35+
if (error) {
36+
return <Text>An error occurred: {error}</Text>;
37+
}
38+
2839
return (
2940
<>
3041
<FavoritesList users={favoritesData} />

examples/cookbook/app/network-requests/__tests__/PhoneBook.test.tsx

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
import { render, screen, waitForElementToBeRemoved } from '@testing-library/react-native';
22
import React from 'react';
3+
import axios from 'axios';
34
import PhoneBook from '../PhoneBook';
45
import { User } from '../types';
5-
import axios from 'axios';
66

77
jest.mock('axios');
8-
const mockedAxios = axios as jest.Mocked<typeof axios>;
98

109
describe('PhoneBook', () => {
1110
it('fetches contacts successfully and renders in list', async () => {
1211
(global.fetch as jest.SpyInstance).mockResolvedValueOnce({
1312
json: jest.fn().mockResolvedValueOnce(DATA),
1413
});
15-
(mockedAxios.get as jest.Mock).mockResolvedValue({ data: DATA });
14+
(axios.get as jest.Mock).mockResolvedValue({ data: DATA });
1615
render(<PhoneBook />);
1716

1817
await waitForElementToBeRemoved(() => screen.getByText(/users data not quite there yet/i));
@@ -21,17 +20,28 @@ describe('PhoneBook', () => {
2120
expect(await screen.findAllByText(/name/i)).toHaveLength(3);
2221
});
2322

24-
it('fetches favorites successfully and renders in list', async () => {
23+
it('fetches favorites successfully and renders all users avatars', async () => {
2524
(global.fetch as jest.SpyInstance).mockResolvedValueOnce({
2625
json: jest.fn().mockResolvedValueOnce(DATA),
2726
});
28-
(mockedAxios.get as jest.Mock).mockResolvedValue({ data: DATA });
27+
(axios.get as jest.Mock).mockResolvedValue({ data: DATA });
2928
render(<PhoneBook />);
3029

3130
await waitForElementToBeRemoved(() => screen.getByText(/figuring out your favorites/i));
3231
expect(await screen.findByText(/my favorites/i)).toBeOnTheScreen();
3332
expect(await screen.findAllByLabelText('favorite-contact-avatar')).toHaveLength(3);
3433
});
34+
35+
it('fails to fetch favorites and renders error message', async () => {
36+
(global.fetch as jest.SpyInstance).mockResolvedValueOnce({
37+
json: jest.fn().mockResolvedValueOnce(DATA),
38+
});
39+
(axios.get as jest.Mock).mockRejectedValueOnce({ message: 'Error fetching favorites' });
40+
render(<PhoneBook />);
41+
42+
await waitForElementToBeRemoved(() => screen.getByText(/figuring out your favorites/i));
43+
expect(await screen.findByText(/error fetching favorites/i)).toBeOnTheScreen();
44+
});
3545
});
3646

3747
const DATA: { results: User[] } = {

website/docs/12.x/cookbook/_meta.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
"name": "basics",
66
"label": "Basic Recipes"
77
},
8+
{
9+
"type": "dir",
10+
"name": "network-requests",
11+
"label": "Network Requests Recipes"
12+
},
813
{
914
"type": "dir",
1015
"name": "state-management",
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
["axios", "fetch"]
Lines changed: 225 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,225 @@
1-
# Fetch
1+
# Axios
2+
3+
## Introduction
4+
5+
Axios is a popular library for making HTTP requests in JavaScript. It is promise-based and has a
6+
simple API that makes it easy to use.
7+
In this guide, we will show you how to mock Axios requests and guard your test suits from unwanted
8+
and unmocked API requests.
9+
10+
:::info
11+
To simulate a real-world scenario, we will use
12+
the [Random User Generator API](https://randomuser.me/) that provides random user data.
13+
:::
14+
15+
## Phonebook Example
16+
17+
Let's assume we have a simple phonebook application that uses Axios for fetching Data from a server.
18+
In our case, we have a list of favorite contacts that we want to display in our application.
19+
20+
This is how the root of the application looks like:
21+
22+
```tsx title=network-requests/axios/Phonebook.tsx
23+
import React, {useEffect, useState} from 'react';
24+
import {User} from './types';
25+
import FavoritesList from './components/FavoritesList';
26+
27+
export default () => {
28+
const [favoritesData, setFavoritesData] = useState<User[]>([]);
29+
const [error, setError] = useState<string | null>(null);
30+
31+
useEffect(() => {
32+
const run = async () => {
33+
try {
34+
const _data = await getAllFavorites();
35+
setFavoritesData(_data);
36+
} catch (e) {
37+
setError(e.message);
38+
}
39+
};
40+
41+
void run();
42+
}, []);
43+
44+
if (error) {
45+
return <Text>An error occurred: {error}</Text>;
46+
}
47+
48+
return (
49+
<FavoritesList users={favoritesData}/>
50+
);
51+
};
52+
53+
```
54+
55+
We fetch the contacts from the server using the `getAllFavorites` function that utilizes Axios.
56+
57+
```tsx title=network-requests/axios/api/getAllFavorites.ts
58+
import axios from 'axios';
59+
import {User} from '../types';
60+
61+
export default async (): Promise<User[]> => {
62+
const res = await axios.get('https://randomuser.me/api/?results=10');
63+
return res.data.results;
64+
};
65+
66+
```
67+
68+
Our `FavoritesList` component is a simple component that displays the list of favorite contacts and
69+
their avatars.
70+
71+
```tsx title=network-requests/axios/components/FavoritesList.tsx
72+
import {FlatList, Image, StyleSheet, Text, View} from 'react-native';
73+
import React, {useCallback} from 'react';
74+
import type {ListRenderItem} from '@react-native/virtualized-lists';
75+
import {User} from '../types';
76+
77+
export default ({users}: { users: User[] }) => {
78+
const renderItem: ListRenderItem<User> = useCallback(({item: {picture}}) => {
79+
return (
80+
<View style={styles.userContainer}>
81+
<Image
82+
source={{uri: picture.thumbnail}}
83+
style={styles.userImage}
84+
accessibilityLabel={'favorite-contact-avatar'}
85+
/>
86+
</View>
87+
);
88+
}, []);
89+
90+
if (users.length === 0) return (
91+
<View style={styles.loaderContainer}>
92+
<Text>Figuring out your favorites...</Text>
93+
</View>
94+
);
95+
96+
return (
97+
<View style={styles.outerContainer}>
98+
<Text>⭐My Favorites</Text>
99+
<FlatList<User>
100+
horizontal
101+
showsHorizontalScrollIndicator={false}
102+
data={users}
103+
renderItem={renderItem}
104+
keyExtractor={(item, index) => `${index}-${item.id.value}`}
105+
/>
106+
</View>
107+
);
108+
};
109+
110+
// Looking for styles?
111+
// Check examples/cookbook/app/network-requests/components/FavoritesList.tsx
112+
const styles =
113+
...
114+
```
115+
116+
## Start testing with a simple test
117+
In our test we will make sure we mock the `axios.get` function to return the data we want.
118+
In this specific case, we will return a list of 3 users.
119+
By using `mockResolvedValueOnce` we gain more grip and prevent the mock from resolving the data
120+
multiple times, which might lead to unexpected behavior.
121+
122+
```tsx title=network-requests/axios/Phonebook.test.tsx
123+
import {render, waitForElementToBeRemoved} from '@testing-library/react-native';
124+
import React from 'react';
125+
import PhoneBook from '../PhoneBook';
126+
import {User} from '../types';
127+
import axios from 'axios';
128+
129+
jest.mock('axios');
130+
131+
describe('PhoneBook', () => {
132+
it('fetches favorites successfully and renders all users avatars', async () => {
133+
// Mock the axios.get function to return the data we want
134+
(axios.get as jest.Mock).mockResolvedValueOnce({data: DATA});
135+
render(<PhoneBook/>);
136+
137+
// Wait for the loader to disappear
138+
await waitForElementToBeRemoved(() => screen.getByText(/figuring out your favorites/i));
139+
expect(await screen.findByText(/my favorites/i)).toBeOnTheScreen();
140+
// All the avatars should be rendered
141+
expect(await screen.findAllByLabelText('favorite-contact-avatar')).toHaveLength(3);
142+
});
143+
});
144+
145+
const DATA: { results: User[] } = {
146+
results: [
147+
{
148+
name: {
149+
title: 'Mrs',
150+
first: 'Ida',
151+
last: 'Kristensen',
152+
},
153+
154+
id: {
155+
name: 'CPR',
156+
value: '250562-5730',
157+
},
158+
picture: {
159+
large: 'https://randomuser.me/api/portraits/women/26.jpg',
160+
medium: 'https://randomuser.me/api/portraits/med/women/26.jpg',
161+
thumbnail: 'https://randomuser.me/api/portraits/thumb/women/26.jpg',
162+
},
163+
cell: '123-4567-890',
164+
},
165+
// For brevity, we have omitted the rest of the users, you can still find them in
166+
// examples/cookbook/app/network-requests/__tests__/PhoneBook.test.tsx
167+
...
168+
],
169+
};
170+
171+
```
172+
173+
## Testing error handling
174+
As we are dealing with network requests, we should also test how our application behaves when the API
175+
request fails. We can mock the `axios.get` function to throw an error and test if our application is
176+
handling the error correctly.
177+
178+
:::note
179+
It is good to note that Axios throws auto. an error when the response status code is not in the range of 2xx.
180+
:::
181+
182+
```tsx title=network-requests/axios/Phonebook.test.tsx
183+
...
184+
it('fails to fetch favorites and renders error message', async () => {
185+
// Mock the axios.get function to throw an error
186+
(axios.get as jest.Mock).mockRejectedValueOnce({ message: 'Error fetching favorites' });
187+
render(<PhoneBook />);
188+
189+
// Wait for the loader to disappear
190+
await waitForElementToBeRemoved(() => screen.getByText(/figuring out your favorites/i));
191+
// Error message should be displayed
192+
expect(await screen.findByText(/error fetching favorites/i)).toBeOnTheScreen();
193+
});
194+
````
195+
196+
## Global guarding against unwanted API requests
197+
198+
As mistakes may happen, we might forget to mock an API request in one of our tests in the future.
199+
To prevent we make unwanted API requests, and alert the developer when it happens, we can globally
200+
mock the `axios` module in our test suite.
201+
202+
```tsx title=__mocks__/axios.ts
203+
const chuckNorrisError = () => {
204+
throw Error(
205+
"Please ensure you mock 'Axios' - Only Chuck Norris is allowed to make API requests when testing ;)",
206+
);
207+
};
208+
209+
export default {
210+
// Mock all the methods to throw an error
211+
get: jest.fn(chuckNorrisError),
212+
post: jest.fn(chuckNorrisError),
213+
put: jest.fn(chuckNorrisError),
214+
delete: jest.fn(chuckNorrisError),
215+
request: jest.fn(chuckNorrisError),
216+
};
217+
```
218+
219+
## Conclusion
220+
Testing a component that makes network requests with Axios is straightforward. By mocking the Axios
221+
requests, we can control the data that is returned and test how our application behaves in different
222+
scenarios, such as when the request is successful or when it fails.
223+
There are many ways to mock Axios requests, and the method you choose will depend on your specific
224+
use case. In this guide, we showed you how to mock Axios requests using Jest's `jest.mock` function
225+
and how to guard against unwanted API requests throughout your test suite.

website/docs/12.x/cookbook/network-requests/msw.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

website/docs/12.x/cookbook/state-management/jotai.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ the developer experience.
1212
Let's assume we have a simple task list component that uses Jotai for state management. The
1313
component has a list of tasks, a text input for typing new task name and a button to add a new task to the list.
1414

15-
```tsx title=jotai/index.test.tsx
15+
```tsx title=state-management/jotai/TaskList.tsx
1616
import * as React from 'react';
1717
import { Pressable, Text, TextInput, View } from 'react-native';
1818
import { useAtom } from 'jotai';
@@ -65,7 +65,7 @@ We can test our `TaskList` component using React Native Testing Library's (RNTL)
6565
function. Although it is sufficient to test the empty state of the `TaskList` component, it is not
6666
enough to test the component with initial tasks present in the list.
6767

68-
```tsx title=jotai/index.test.tsx
68+
```tsx title=status-management/jotai/__tests__/TaskList.test.tsx
6969
import * as React from 'react';
7070
import { render, screen, userEvent } from '@testing-library/react-native';
7171
import { renderWithAtoms } from './test-utils';
@@ -88,7 +88,7 @@ initial values. We can create a custom render function that uses Jotai's `useHyd
8888
hydrate the atoms with initial values. This function will accept the initial atoms and their
8989
corresponding values as an argument.
9090

91-
```tsx title=test-utils.tsx
91+
```tsx title=status-management/jotai/test-utils.tsx
9292
import * as React from 'react';
9393
import { render } from '@testing-library/react-native';
9494
import { useHydrateAtoms } from 'jotai/utils';
@@ -144,7 +144,7 @@ We can now use the `renderWithAtoms` function to render the `TaskList` component
144144
In our test, we populated only one atom and its initial value, but you can add other Jotai atoms and their corresponding values to the initialValues array as needed.
145145
:::
146146

147-
```tsx title=jotai/index.test.tsx
147+
```tsx title=status-management/jotai/__tests__/TaskList.test.tsx
148148
=======
149149
const INITIAL_TASKS: Task[] = [{ id: '1', title: 'Buy bread' }];
150150

@@ -173,7 +173,7 @@ test('renders a to do list with 1 items initially, and adds a new item', async (
173173
In several cases, you might need to change an atom's state outside a React component. In our case,
174174
we have a set of functions to get tasks and set tasks, which change the state of the task list atom.
175175

176-
```tsx title=state.ts
176+
```tsx title=state-management/jotai/state.ts
177177
import { atom, createStore } from 'jotai';
178178
import { Task } from './types';
179179

@@ -201,7 +201,7 @@ the initial to-do items in the store and then checking if the functions work as
201201
No special setup is required to test these functions, as `store.set` is available by default by
202202
Jotai.
203203

204-
```tsx title=jotai/index.test.tsx
204+
```tsx title=state-management/jotai/__tests__/TaskList.test.tsx
205205
import { addTask, getAllTasks, store, tasksAtom } from './state';
206206

207207
//...

0 commit comments

Comments
 (0)