-
Notifications
You must be signed in to change notification settings - Fork 273
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
docs(cookbook): network requests recipes #1655
Merged
mdjastrzebski
merged 13 commits into
callstack:main
from
vanGalilea:docs/cookbook/net-reqs
Sep 18, 2024
Merged
Changes from 10 commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
19dd3bc
squash prev commits
35a9e9e
remove unneeded axios mock
81e7462
set maxWorkers=2
2c33c44
run with a slow test reporter
5a5cab2
revert: run with a slow test reporter
4a2fb86
Add url check to mock and further reading and alternatives
7fa6eb0
Merge branch 'callstack:main' into docs/cookbook/net-reqs
vanGalilea 336c00e
use MSW for all API calls in cookbook test suits
f253045
Comments with implem. explanation
ba36a0f
Arrange docs initially to reflect new scenario and remove jest.setTim…
5d55fcd
updating docs (1)
ded40d1
updating docs (2)
d7acda3
updating docs with global guarding and conclusion
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(() => { | ||
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; |
36 changes: 36 additions & 0 deletions
36
examples/cookbook/app/network-requests/__tests__/PhoneBook.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { render, screen, waitForElementToBeRemoved } from '@testing-library/react-native'; | ||
import React from 'react'; | ||
import PhoneBook from '../PhoneBook'; | ||
import { | ||
mockServerFailureForGetAllContacts, | ||
mockServerFailureForGetAllFavorites, | ||
} from './test-utils'; | ||
|
||
describe('PhoneBook', () => { | ||
it('fetches all contacts and favorites successfully and renders lists in sections correctly', async () => { | ||
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. should the be a mock call here? Seems like the timeout might be caused by different test order execution between the test runs locally and on the CI. |
||
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: ida.kristensen@example.com')).toBeOnTheScreen(); | ||
expect(await screen.findAllByText(/name/i)).toHaveLength(3); | ||
expect(await screen.findByText(/my favorites/i)).toBeOnTheScreen(); | ||
expect(await screen.findAllByLabelText('favorite-contact-avatar')).toHaveLength(3); | ||
}); | ||
|
||
it('fails to fetch all contacts and renders error message', async () => { | ||
mockServerFailureForGetAllContacts(); | ||
render(<PhoneBook />); | ||
|
||
await waitForElementToBeRemoved(() => screen.getByText(/users data not quite there yet/i)); | ||
expect(await screen.findByText(/error fetching contacts/i)).toBeOnTheScreen(); | ||
}); | ||
|
||
it('fails to fetch favorites and renders error message', async () => { | ||
mockServerFailureForGetAllFavorites(); | ||
render(<PhoneBook />); | ||
|
||
await waitForElementToBeRemoved(() => screen.getByText(/figuring out your favorites/i)); | ||
expect(await screen.findByText(/error fetching favorites/i)).toBeOnTheScreen(); | ||
}); | ||
}); |
109 changes: 109 additions & 0 deletions
109
examples/cookbook/app/network-requests/__tests__/test-utils.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import { User } from '../types'; | ||
import {http, HttpResponse} from "msw"; | ||
import {setupServer} from "msw/node"; | ||
|
||
// Define request handlers and response resolvers for random user API. | ||
// By default, we always return the happy path response. | ||
const handlers = [ | ||
http.get('https://randomuser.me/api/*', () => { | ||
return HttpResponse.json(DATA); | ||
}), | ||
]; | ||
|
||
export const server = setupServer(...handlers); | ||
|
||
export const mockServerFailureForGetAllContacts = () => { | ||
server.use( | ||
http.get('https://randomuser.me/api/', ({ request }) => { | ||
// Construct a URL instance out of the intercepted request. | ||
const url = new URL(request.url); | ||
// Read the "results" URL query parameter using the "URLSearchParams" API. | ||
const resultsLength = url.searchParams.get('results'); | ||
// Simulate a server error for the get all contacts request. | ||
// We check if the "results" query parameter is set to "25" | ||
// to know it's the correct request to mock, in our case get all contacts. | ||
if (resultsLength === '25') { | ||
vanGalilea marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return new HttpResponse(null, { status: 500 }); | ||
} | ||
|
||
return HttpResponse.json(DATA); | ||
}), | ||
); | ||
}; | ||
|
||
export const mockServerFailureForGetAllFavorites = () => { | ||
server.use( | ||
http.get('https://randomuser.me/api/', ({ request }) => { | ||
// Construct a URL instance out of the intercepted request. | ||
const url = new URL(request.url); | ||
// Read the "results" URL query parameter using the "URLSearchParams" API. | ||
const resultsLength = url.searchParams.get('results'); | ||
// Simulate a server error for the get all favorites request. | ||
// We check if the "results" query parameter is set to "10" | ||
// to know it's the correct request to mock, in our case get all favorites. | ||
if (resultsLength === '10') { | ||
return new HttpResponse(null, { status: 500 }); | ||
} | ||
|
||
return HttpResponse.json(DATA); | ||
}), | ||
); | ||
}; | ||
export const DATA: { results: User[] } = { | ||
vanGalilea marked this conversation as resolved.
Show resolved
Hide resolved
|
||
results: [ | ||
{ | ||
name: { | ||
title: 'Mrs', | ||
first: 'Ida', | ||
last: 'Kristensen', | ||
}, | ||
email: 'ida.kristensen@example.com', | ||
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: 'elijah.ellis@example.com', | ||
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: 'miro.halko@example.com', | ||
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', | ||
}, | ||
], | ||
}; |
10 changes: 10 additions & 0 deletions
10
examples/cookbook/app/network-requests/api/getAllContacts.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; |
10 changes: 10 additions & 0 deletions
10
examples/cookbook/app/network-requests/api/getAllFavorites.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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=10'); | ||
if (!res.ok) { | ||
throw new Error(`Error fetching favorites`); | ||
} | ||
const json = await res.json(); | ||
return json.results; | ||
}; |
60 changes: 60 additions & 0 deletions
60
examples/cookbook/app/network-requests/components/ContactsList.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' }, | ||
}); |
59 changes: 59 additions & 0 deletions
59
examples/cookbook/app/network-requests/components/FavoritesList.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' }, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 />; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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 comment
The 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.
With my approach I intended to keep things more straight-forward.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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 ;)