Skip to content

Commit

Permalink
Add React Query command (#26)
Browse files Browse the repository at this point in the history
This follows the template that [we previously coded in
react-native-templates](https://github.com/thoughtbot/react-native-templates/pull/1/files#diff-fc130cd2eebfc7cbeb229f509cb090c6fb5837a169b9270709cfa73ff46c9a56).

* Installs React Query
* Installs testing/mocking utility, MSW
* Creates a mocking and testing strategy
* Adds an example API call and mock and test

For now, the "create" command automatically uses React Query, but we
will likely decide to prompt the user if they'd like to use this or
Apollo (for GraphQL) in the future.

---------

Co-authored-by: Frida Casas <frida@thoughtbot.com>
Co-authored-by: Rakesh Arunachalam <rakesh@thoughtbot.com>
  • Loading branch information
3 people committed May 3, 2024
1 parent 302f80e commit 21bfc00
Show file tree
Hide file tree
Showing 18 changed files with 378 additions and 95 deletions.
76 changes: 0 additions & 76 deletions src/commands/__tests__/reactQuery.test.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/commands/createApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ async function printIntro(appName: string) {
- ESLint
- Jest, React Native Testing Library
- React Navigation
- Intuitive directory structure
- TanStack Query (formerly known as React Query)
`);

if (!globals.interactive) {
Expand Down
10 changes: 3 additions & 7 deletions src/util/addToGitignore.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import fs from 'fs-extra';
import path from 'path';
import getProjectDir from './getProjectDir';
import appendToFile from './appendToFile';

/**
* lines should be separated by newlines
*/
export default async function addToGitignore(lines: string) {
return fs.appendFile(
path.join(await getProjectDir(), '.gitignore'),
`\n${lines}`,
);
return appendToFile('.gitignore', lines);
}
12 changes: 12 additions & 0 deletions src/util/appendToFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import fs from 'fs-extra';
import path from 'path';
import getProjectDir from './getProjectDir';
/**
* lines should be separated by newlines
*/
export default async function appendToFile(filename: string, lines: string) {
return fs.appendFile(
path.join(await getProjectDir(), filename),
`\n${lines}`,
);
}
9 changes: 7 additions & 2 deletions templates/boilerplate/App.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { NavigationContainer } from '@react-navigation/native';

import { QueryClientProvider } from '@tanstack/react-query';
import Providers, { Provider } from 'src/components/Providers';
import RootNavigator from 'src/navigators/RootNavigator';
import queryClient from 'src/util/api/queryClient';

// Add providers to this array
// Add providers to this array. They will be wrapped around the app, with the
// first items in the array wrapping the last items in the array.
const providers: Provider[] = [
(children) => <NavigationContainer>{children}</NavigationContainer>,
(children) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
),
// CODEGEN:BELT:PROVIDERS - do not remove
];

Expand Down
14 changes: 14 additions & 0 deletions templates/boilerplate/jest.setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import '@testing-library/jest-native/extend-expect';
import { configure } from '@testing-library/react-native';
import mockSafeAreaContext from 'react-native-safe-area-context/jest/mock';
import mockBackHandler from 'react-native/Libraries/Utilities/__mocks__/BackHandler.js';
import server from 'src/test/server';
import queryClient from 'src/util/api/queryClient';

beforeEach(() => {
jest.clearAllMocks();
Expand Down Expand Up @@ -35,6 +37,18 @@ jest.mock('@react-native-async-storage/async-storage', () =>

jest.mock('react-native-keyboard-aware-scroll-view');

// listen with MSW server. Individual tests can pass mocks to 'render' function
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterAll(() => server.close());

beforeEach(() => {
server.resetHandlers();
});

afterEach(() => {
queryClient.clear();
});

// configure debug output for RN Testing Library
// is way too verbose by default. Only include common
// props that might affect test failure.
Expand Down
4 changes: 4 additions & 0 deletions templates/boilerplate/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,13 @@
"@react-navigation/bottom-tabs": "^6.5.20",
"@react-navigation/native": "^6.1.10",
"@react-navigation/native-stack": "^6.9.18",
"@tanstack/react-query": "^5.32.1",
"axios": "^1.6.8",
"expo": "^50.0.17",
"expo-status-bar": "~1.11.1",
"jest": "^29.3.1",
"jest-expo": "~50.0.2",
"msw": "^2.2.14",
"react": "18.2.0",
"react-native": "0.73.6",
"react-native-keyboard-aware-scrollview": "^2.1.0",
Expand All @@ -40,6 +43,7 @@
"@thoughtbot/eslint-config": "^1.0.2",
"@types/jest": "^29.5.12",
"@types/react": "~18.2.73",
"@types/react-test-renderer": "^18.0.7",
"babel-jest": "^29.7.0",
"create-belt-app": "^0.4.0",
"eslint": "^8.56.0",
Expand Down
29 changes: 25 additions & 4 deletions templates/boilerplate/src/__tests__/App.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,31 @@
import { screen } from '@testing-library/react-native';

import RootNavigator from 'src/navigators/RootNavigator';
import render from 'src/test/render';
import mock from 'src/test/mock';
import { renderApplication } from 'src/test/render';

test('renders', async () => {
// We would not normally recommend fake timers, but the tests are currently
// throwing a "not wrapped in act" warning after this test finishes. One
// option is to put a `await waitForUpdates()` at the end of the test, but
// fake timers also work here until we find a better solution. The stack trace
// seems to point to React Navigation bottom tabs.
jest.useFakeTimers();
render(<RootNavigator />);
expect(await screen.findByText(/Open up App.tsx/)).toBeDefined();

const mocks = [mockCoffees()];

renderApplication({ mocks });

expect(await screen.findByRole('header', { name: 'Mocha' })).toBeDefined();
});

function mockCoffees() {
return mock.get('coffee/hot', {
response: [
{
id: 1,
title: 'Mocha',
image: 'htps://placehold.it/200x200',
},
],
});
}
49 changes: 49 additions & 0 deletions templates/boilerplate/src/components/ExampleCoffees.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useQuery } from '@tanstack/react-query';
import { FlatList, Image, Text, View } from 'react-native';
import api, { Coffee as CoffeeType } from 'src/util/api/api';

// TODO: sample data, remove
export default function ExampleCoffees() {
const { data } = useQuery({ queryKey: ['coffee'], queryFn: api.coffee });

return (
<>
<Text style={{ fontWeight: '600', fontSize: 24 }}>Coffees</Text>
<FlatList
data={data?.slice(0, 4)}
numColumns={2}
scrollEnabled={false}
renderItem={({ item }) => <Coffee coffee={item} />}
keyExtractor={(item) => item.id.toString()}
style={{ flexGrow: 0 }}
/>
</>
);
}

function Coffee({ coffee }: { coffee: CoffeeType }) {
const { title, image } = coffee;

return (
<View
style={{
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'salmon',
borderRadius: 10,
padding: 16,
margin: 10,
}}
>
<Text accessibilityRole="header" style={{ fontWeight: '600' }}>
{title}
</Text>
<Image
width={100}
height={100}
source={{ uri: image }}
accessibilityIgnoresInvertColors
/>
</View>
);
}
2 changes: 2 additions & 0 deletions templates/boilerplate/src/screens/HomeScreen/HomeScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useNavigation } from '@react-navigation/native';
import { StatusBar } from 'expo-status-bar';
import { Button, StyleSheet, Text, View } from 'react-native';
import ExampleCoffees from 'src/components/ExampleCoffees';
import { HomeScreenProp } from 'src/navigators/navigatorTypes';

export default function HomeScreen() {
Expand All @@ -14,6 +15,7 @@ export default function HomeScreen() {
onPress={() => navigation.navigate('Information', { owner: 'Will' })}
/>
<StatusBar style="auto" />
<ExampleCoffees />
</View>
);
}
Expand Down

0 comments on commit 21bfc00

Please sign in to comment.