-
Notifications
You must be signed in to change notification settings - Fork 274
docs(cookbook): network requests recipes #1655
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
19dd3bc
35a9e9e
81e7462
2c33c44
5a5cab2
4a2fb86
7fa6eb0
336c00e
f253045
ba36a0f
5d55fcd
ded40d1
d7acda3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
const chuckNorrisError = () => { | ||
throw Error( | ||
"Please ensure you mock 'Axios' - Only Chuck Norris is allowed to make API requests when testing ;)", | ||
); | ||
}; | ||
|
||
export default { | ||
get: jest.fn(chuckNorrisError), | ||
vanGalilea marked this conversation as resolved.
Show resolved
Hide resolved
|
||
post: jest.fn(chuckNorrisError), | ||
put: jest.fn(chuckNorrisError), | ||
delete: jest.fn(chuckNorrisError), | ||
request: jest.fn(chuckNorrisError), | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import React, { useEffect, useState } from 'react'; | ||
import { Text } from 'react-native'; | ||
import { User } from './types'; | ||
import ContactsList from './components/ContactsList'; | ||
import FavoritesList from './components/FavoritesList'; | ||
import getAllContacts from './api/getAllContacts'; | ||
import getAllFavorites from './api/getAllFavorites'; | ||
|
||
export default () => { | ||
const [usersData, setUsersData] = useState<User[]>([]); | ||
const [favoritesData, setFavoritesData] = useState<User[]>([]); | ||
const [error, setError] = useState<string | null>(null); | ||
|
||
useEffect(() => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thinking out loud, perhaps we should showcase usage with TanStack Query instead of manual promise fetching. Implementing data fetching using useEffect to avoid race conditions is verbose. Wdyt @vanGalilea ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Showcase is a very good idea, as it is become and industry standard the last years. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let me add it up, this week 👍🏻 In the meanwhile, you can review the rest ;) |
||
const _getAllContacts = async () => { | ||
const _data = await getAllContacts(); | ||
setUsersData(_data); | ||
}; | ||
const _getAllFavorites = async () => { | ||
const _data = await getAllFavorites(); | ||
setFavoritesData(_data); | ||
}; | ||
|
||
const run = async () => { | ||
try { | ||
await Promise.all([_getAllContacts(), _getAllFavorites()]); | ||
} catch (e) { | ||
const message = isErrorWithMessage(e) ? e.message : 'Something went wrong'; | ||
setError(message); | ||
} | ||
}; | ||
|
||
void run(); | ||
}, []); | ||
|
||
if (error) { | ||
return <Text>An error occurred: {error}</Text>; | ||
} | ||
|
||
return ( | ||
<> | ||
<FavoritesList users={favoritesData} /> | ||
<ContactsList users={usersData} /> | ||
</> | ||
); | ||
}; | ||
|
||
const isErrorWithMessage = ( | ||
e: unknown, | ||
): e is { | ||
message: string; | ||
} => typeof e === 'object' && e !== null && 'message' in e; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
import { render, screen, waitForElementToBeRemoved } from '@testing-library/react-native'; | ||
import React from 'react'; | ||
import axios from 'axios'; | ||
import PhoneBook from '../PhoneBook'; | ||
import { User } from '../types'; | ||
|
||
jest.mock('axios'); | ||
|
||
jest.setTimeout(10000); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When this test suit runs in CI, the default 5s is not sufficient. Any idea what might be causing this on CI? @mdjastrzebski? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. According to my measuring:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems like the spec. tests "renders an empty task list" and "PhoneBook fetches contacts successfully and renders in list" are the slowest as they're the 1st tests in the file 🤷🏻 |
||
describe('PhoneBook', () => { | ||
it('fetches contacts successfully and renders in list', async () => { | ||
(global.fetch as jest.Mock).mockResolvedValueOnce({ | ||
vanGalilea marked this conversation as resolved.
Show resolved
Hide resolved
|
||
ok: true, | ||
json: jest.fn().mockResolvedValueOnce(DATA), | ||
}); | ||
(axios.get as jest.Mock).mockResolvedValue({ data: DATA }); | ||
render(<PhoneBook />); | ||
|
||
await waitForElementToBeRemoved(() => screen.getByText(/users data not quite there yet/i)); | ||
expect(await screen.findByText('Name: Mrs Ida Kristensen')).toBeOnTheScreen(); | ||
expect(await screen.findByText('Email: [email protected]')).toBeOnTheScreen(); | ||
expect(await screen.findAllByText(/name/i)).toHaveLength(3); | ||
}); | ||
|
||
it('fails to fetch contacts and renders error message', async () => { | ||
(global.fetch as jest.Mock).mockResolvedValueOnce({ | ||
ok: false, | ||
}); | ||
(axios.get as jest.Mock).mockResolvedValue({ data: DATA }); | ||
render(<PhoneBook />); | ||
|
||
await waitForElementToBeRemoved(() => screen.getByText(/users data not quite there yet/i)); | ||
expect(await screen.findByText(/error fetching contacts/i)).toBeOnTheScreen(); | ||
}); | ||
|
||
it('fetches favorites successfully and renders all users avatars', async () => { | ||
(global.fetch as jest.Mock).mockResolvedValueOnce({ | ||
ok: true, | ||
json: jest.fn().mockResolvedValueOnce(DATA), | ||
}); | ||
(axios.get as jest.Mock).mockResolvedValue({ data: DATA }); | ||
render(<PhoneBook />); | ||
|
||
await waitForElementToBeRemoved(() => screen.getByText(/figuring out your favorites/i)); | ||
expect(await screen.findByText(/my favorites/i)).toBeOnTheScreen(); | ||
expect(await screen.findAllByLabelText('favorite-contact-avatar')).toHaveLength(3); | ||
}); | ||
|
||
it('fails to fetch favorites and renders error message', async () => { | ||
(global.fetch as jest.Mock).mockResolvedValueOnce({ | ||
ok: true, | ||
json: jest.fn().mockResolvedValueOnce(DATA), | ||
}); | ||
(axios.get as jest.Mock).mockRejectedValueOnce({ message: 'Error fetching favorites' }); | ||
render(<PhoneBook />); | ||
|
||
await waitForElementToBeRemoved(() => screen.getByText(/figuring out your favorites/i)); | ||
expect(await screen.findByText(/error fetching favorites/i)).toBeOnTheScreen(); | ||
}); | ||
}); | ||
|
||
const DATA: { results: User[] } = { | ||
results: [ | ||
{ | ||
name: { | ||
title: 'Mrs', | ||
first: 'Ida', | ||
last: 'Kristensen', | ||
}, | ||
email: '[email protected]', | ||
id: { | ||
name: 'CPR', | ||
value: '250562-5730', | ||
}, | ||
picture: { | ||
large: 'https://randomuser.me/api/portraits/women/26.jpg', | ||
medium: 'https://randomuser.me/api/portraits/med/women/26.jpg', | ||
thumbnail: 'https://randomuser.me/api/portraits/thumb/women/26.jpg', | ||
}, | ||
cell: '123-4567-890', | ||
}, | ||
{ | ||
name: { | ||
title: 'Mr', | ||
first: 'Elijah', | ||
last: 'Ellis', | ||
}, | ||
email: '[email protected]', | ||
id: { | ||
name: 'TFN', | ||
value: '138117486', | ||
}, | ||
picture: { | ||
large: 'https://randomuser.me/api/portraits/men/53.jpg', | ||
medium: 'https://randomuser.me/api/portraits/med/men/53.jpg', | ||
thumbnail: 'https://randomuser.me/api/portraits/thumb/men/53.jpg', | ||
}, | ||
cell: '123-4567-890', | ||
}, | ||
{ | ||
name: { | ||
title: 'Mr', | ||
first: 'Miro', | ||
last: 'Halko', | ||
}, | ||
email: '[email protected]', | ||
id: { | ||
name: 'HETU', | ||
value: 'NaNNA945undefined', | ||
}, | ||
picture: { | ||
large: 'https://randomuser.me/api/portraits/men/17.jpg', | ||
medium: 'https://randomuser.me/api/portraits/med/men/17.jpg', | ||
thumbnail: 'https://randomuser.me/api/portraits/thumb/men/17.jpg', | ||
}, | ||
cell: '123-4567-890', | ||
}, | ||
], | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { User } from '../types'; | ||
|
||
export default async (): Promise<User[]> => { | ||
const res = await fetch('https://randomuser.me/api/?results=25'); | ||
if (!res.ok) { | ||
throw new Error(`Error fetching contacts`); | ||
} | ||
const json = await res.json(); | ||
return json.results; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import axios from 'axios'; | ||
import { User } from '../types'; | ||
|
||
export default async (): Promise<User[]> => { | ||
const res = await axios.get('https://randomuser.me/api/?results=10'); | ||
return res.data.results; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import { FlatList, Image, StyleSheet, Text, View } from 'react-native'; | ||
import React, { useCallback } from 'react'; | ||
import type { ListRenderItem } from '@react-native/virtualized-lists'; | ||
import { User } from '../types'; | ||
|
||
export default ({ users }: { users: User[] }) => { | ||
const renderItem: ListRenderItem<User> = useCallback( | ||
({ item: { name, email, picture, cell }, index }) => { | ||
const { title, first, last } = name; | ||
const backgroundColor = index % 2 === 0 ? '#f9f9f9' : '#fff'; | ||
return ( | ||
<View style={[{ backgroundColor }, styles.userContainer]}> | ||
<Image source={{ uri: picture.thumbnail }} style={styles.userImage} /> | ||
<View> | ||
<Text> | ||
Name: {title} {first} {last} | ||
</Text> | ||
<Text>Email: {email}</Text> | ||
<Text>Mobile: {cell}</Text> | ||
</View> | ||
</View> | ||
); | ||
}, | ||
[], | ||
); | ||
|
||
if (users.length === 0) return <FullScreenLoader />; | ||
|
||
return ( | ||
<View> | ||
<FlatList<User> | ||
data={users} | ||
renderItem={renderItem} | ||
keyExtractor={(item, index) => `${index}-${item.id.value}`} | ||
/> | ||
</View> | ||
); | ||
}; | ||
const FullScreenLoader = () => { | ||
return ( | ||
<View style={styles.loaderContainer}> | ||
<Text>Users data not quite there yet...</Text> | ||
</View> | ||
); | ||
}; | ||
|
||
const styles = StyleSheet.create({ | ||
userContainer: { | ||
padding: 16, | ||
flexDirection: 'row', | ||
alignItems: 'center', | ||
}, | ||
userImage: { | ||
width: 50, | ||
height: 50, | ||
borderRadius: 24, | ||
marginRight: 16, | ||
}, | ||
loaderContainer: { flex: 1, justifyContent: 'center', alignItems: 'center' }, | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import { FlatList, Image, StyleSheet, Text, View } from 'react-native'; | ||
import React, { useCallback } from 'react'; | ||
import type { ListRenderItem } from '@react-native/virtualized-lists'; | ||
import { User } from '../types'; | ||
|
||
export default ({ users }: { users: User[] }) => { | ||
const renderItem: ListRenderItem<User> = useCallback(({ item: { picture } }) => { | ||
return ( | ||
<View style={styles.userContainer}> | ||
<Image | ||
source={{ uri: picture.thumbnail }} | ||
style={styles.userImage} | ||
accessibilityLabel={'favorite-contact-avatar'} | ||
/> | ||
</View> | ||
); | ||
}, []); | ||
|
||
if (users.length === 0) return <FullScreenLoader />; | ||
|
||
return ( | ||
<View style={styles.outerContainer}> | ||
<Text>⭐My Favorites</Text> | ||
<FlatList<User> | ||
horizontal | ||
showsHorizontalScrollIndicator={false} | ||
data={users} | ||
renderItem={renderItem} | ||
keyExtractor={(item, index) => `${index}-${item.id.value}`} | ||
/> | ||
</View> | ||
); | ||
}; | ||
const FullScreenLoader = () => { | ||
return ( | ||
<View style={styles.loaderContainer}> | ||
<Text>Figuring out your favorites...</Text> | ||
</View> | ||
); | ||
}; | ||
|
||
const styles = StyleSheet.create({ | ||
outerContainer: { | ||
padding: 8, | ||
}, | ||
userContainer: { | ||
padding: 8, | ||
flexDirection: 'row', | ||
alignItems: 'center', | ||
}, | ||
userImage: { | ||
width: 52, | ||
height: 52, | ||
borderRadius: 36, | ||
borderColor: '#9b6dff', | ||
borderWidth: 2, | ||
}, | ||
loaderContainer: { height: 52, justifyContent: 'center', alignItems: 'center' }, | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import * as React from 'react'; | ||
import PhoneBook from './PhoneBook'; | ||
|
||
export default function Example() { | ||
return <PhoneBook />; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
export type User = { | ||
name: { | ||
title: string; | ||
first: string; | ||
last: string; | ||
}; | ||
email: string; | ||
id: { | ||
name: string; | ||
value: string; | ||
}; | ||
picture: { | ||
large: string; | ||
medium: string; | ||
thumbnail: string; | ||
}; | ||
cell: string; | ||
}; |
Uh oh!
There was an error while loading. Please reload this page.