Integrating React Native with GraphQL: A Comprehensive Guide
Tags: reactnative, graphql, testing, react
Published: 2024-06-29 14:16:57

Heys devs!

React Native is a powerful tool for developing cross-platform mobile applications, while GraphQL offers a flexible and efficient approach to consuming APIs. Together, they can make app development faster and less error-prone. In this post, we will explore how to set up and use GraphQL in a React Native application with TypeScript, including installation, code examples (queries and mutations), tests, and best practices.

### Installation

#### 1. Setting Up the Environment

First, ensure you have your React Native development environment set up. If you haven't done this yet, follow the instructions in the [official documentation](https://reactnative.dev/docs/environment-setup) to configure the React Native CLI.

#### 2. Creating a New React Native Project

Create a new React Native project using the following command:

```bash
npx react-native init MyGraphQLApp --template react-native-template-typescript
cd MyGraphQLApp
```

#### 3. Installing Necessary Dependencies

To use GraphQL with React Native, we'll need some additional libraries. The main one is Apollo Client, a popular GraphQL client library for JavaScript.

Install Apollo Client and other necessary dependencies:

```bash
npm install @apollo/client graphql
```

### Setting Up Apollo Client

#### 1. Configuring Apollo Client

Create a file named `ApolloClient.ts` in your project's `src` folder:

```typescript
// src/ApolloClient.ts

import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';

const httpLink = createHttpLink({
  uri: 'https://your-graphql-endpoint.com/graphql',
});

const authLink = setContext((_, { headers }) => {
  const token = 'your-auth-token'; // Replace with your authentication token
  return {
    headers: {
      ...headers,
      authorization: token ? `Bearer ${token}` : '',
    }
  };
});

const client = new ApolloClient({
  link: authLink.concat(httpLink),
  cache: new InMemoryCache()
});

export default client;
```

#### 2. Setting Up ApolloProvider

In the `App.tsx` file, configure ApolloProvider to provide the Apollo client to the entire application:

```typescript
// App.tsx

import React from 'react';
import { ApolloProvider } from '@apollo/client';
import client from './src/ApolloClient';
import HomeScreen from './src/HomeScreen';

const App: React.FC = () => {
  return (
    <ApolloProvider client={client}>
      <HomeScreen />
    </ApolloProvider>
  );
};

export default App;
```

### Consuming Data with GraphQL

#### 1. Writing a GraphQL Query

Create a `queries.ts` file in the `src` folder to store your queries:

```typescript
// src/queries.ts

import { gql } from '@apollo/client';

export const GET_DATA = gql`
  query GetData {
    data {
      id
      name
      description
    }
  }
`;
```

#### 2. Using the Query in a Component

In your `HomeScreen.tsx` component, use the GraphQL query with Apollo's `useQuery` hook:

```typescript
// src/HomeScreen.tsx

import React from 'react';
import { View, Text, ActivityIndicator, FlatList } from 'react-native';
import { useQuery } from '@apollo/client';
import { GET_DATA } from './queries';

interface DataItem {
  id: string;
  name: string;
  description: string;
}

interface GetDataResult {
  data: DataItem[];
}

const HomeScreen: React.FC = () => {
  const { loading, error, data } = useQuery<GetDataResult>(GET_DATA);

  if (loading) return <ActivityIndicator testID="loading" size="large" color="#0000ff" />;
  if (error) return <Text>Error: {error.message}</Text>;

  return (
    <View>
      <FlatList
        data={data?.data}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => (
          <View>
            <Text>{item.name}</Text>
            <Text>{item.description}</Text>
          </View>
        )}
      />
    </View>
  );
};

export default HomeScreen;
```

### Executing Mutations with GraphQL

#### 1. Writing a GraphQL Mutation

Create a `mutations.ts` file in the `src` folder to store your mutations:

```typescript
// src/mutations.ts

import { gql } from '@apollo/client';

export const ADD_DATA = gql`
  mutation AddData($name: String!, $description: String!) {
    addData(name: $name, description: $description) {
      id
      name
      description
    }
  }
`;
```

#### 2. Using the Mutation in a Component

In your `HomeScreen.tsx` component, use the GraphQL mutation with Apollo's `useMutation` hook:

```typescript
// src/HomeScreen.tsx

import React, { useState } from 'react';
import { View, Text, TextInput, Button, ActivityIndicator, FlatList } from 'react-native';
import { useQuery, useMutation } from '@apollo/client';
import { GET_DATA } from './queries';
import { ADD_DATA } from './mutations';

interface DataItem {
  id: string;
  name: string;
  description: string;
}

interface GetDataResult {
  data: DataItem[];
}

interface AddDataVars {
  name: string;
  description: string;
}

const HomeScreen: React.FC = () => {
  const { loading, error, data } = useQuery<GetDataResult>(GET_DATA);
  const [addData] = useMutation<DataItem, AddDataVars>(ADD_DATA);
  
  const [name, setName] = useState('');
  const [description, setDescription] = useState('');

  const handleAddData = () => {
    addData({ 
      variables: { name, description },
      refetchQueries: [{ query: GET_DATA }]
    });
  };

  if (loading) return <ActivityIndicator testID="loading" size="large" color="#0000ff" />;
  if (error) return <Text>Error: {error.message}</Text>;

  return (
    <View>
      <TextInput 
        placeholder="Name"
        value={name}
        onChangeText={setName}
        testID="name-input"
      />
      <TextInput 
        placeholder="Description"
        value={description}
        onChangeText={setDescription}
        testID="description-input"
      />
      <Button title="Add Data" onPress={handleAddData} testID="add-button" />
      
      <FlatList
        data={data?.data}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => (
          <View>
            <Text>{item.name}</Text>
            <Text>{item.description}</Text>
          </View>
        )}
      />
    </View>
  );
};

export default HomeScreen;
```

### Setting Up Tests

#### 1. Installing Test Dependencies

Let's install the necessary libraries for testing. We will use `jest`, `react-native-testing-library`, and `@testing-library/react-native`.

```bash
npm install --save-dev jest @testing-library/react-native @testing-library/jest-native @types/jest
```

#### 2. Configuring Jest

Add the Jest configuration to your `package.json`:

```json
"jest": {
  "preset": "react-native",
  "setupFilesAfterEnv": [
    "@testing-library/jest-native/extend-expect"
  ],
  "transformIgnorePatterns": [
    "node_modules/(?!(jest-)?react-native|@react-native|@react-native-community|@testing-library|@react-navigation)"
  ]
}
```

### Writing Tests

#### 1. Testing Apollo Client

We'll create a mock of the Apollo Client to use in our tests. Create a file called `ApolloMockProvider.tsx` in the `src/test` folder:

```typescript
// src/test/ApolloMockProvider.tsx

import React, { ReactNode } from 'react';
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
import { MockedProvider } from '@apollo/client/testing';

interface Props {
  children: ReactNode;
  mocks: any[];
}

const ApolloMockProvider: React.FC<Props> = ({ children, mocks }) => {
  const client = new ApolloClient({
    cache: new InMemoryCache(),
  });

  return (
    <MockedProvider mocks={mocks} addTypename={false}>
      <ApolloProvider client={client}>
        {children}
      </ApolloProvider>
    </MockedProvider>
  );
};

export default ApolloMockProvider;
```

#### 2. Testing the `HomeScreen` Component

Create a test file called `HomeScreen.test.tsx` in the `src/__tests__` folder:

```typescript
// src/__tests__/HomeScreen.test.tsx

import React from 'react';
import { render, waitFor, fireEvent } from '@testing-library/react-native';
import HomeScreen from '../HomeScreen';
import ApolloMockProvider from '../test/ApolloMockProvider';
import { GET_DATA, ADD_DATA } from '../queries';
import { MockedResponse } from '@apollo/client/testing';

const mocks: MockedResponse[] = [
  {
    request: {
      query: GET_DATA,
    },
    result: {
      data: {
        data: [
          { id: '1', name: 'Test Name', description:
 'Test Description' },
        ],
      },
    },
  },
  {
    request: {
      query: ADD_DATA,
      variables: {
        name: 'New Name',
        description: 'New Description',
      },
    },
    result: {
      data: {
        addData: {
          id: '2',
          name: 'New Name',
          description: 'New Description',
        },
      },
    },
  },
];

describe('HomeScreen', () => {
  it('renders loading state initially', () => {
    const { getByTestId } = render(
      <ApolloMockProvider mocks={[]}>
        <HomeScreen />
      </ApolloMockProvider>
    );
    expect(getByTestId('loading')).toBeTruthy();
  });

  it('renders data correctly', async () => {
    const { getByText } = render(
      <ApolloMockProvider mocks={mocks}>
        <HomeScreen />
      </ApolloMockProvider>
    );

    await waitFor(() => {
      expect(getByText('Test Name')).toBeTruthy();
      expect(getByText('Test Description')).toBeTruthy();
    });
  });

  it('adds new data correctly', async () => {
    const { getByPlaceholderText, getByText } = render(
      <ApolloMockProvider mocks={mocks}>
        <HomeScreen />
      </ApolloMockProvider>
    );

    const nameInput = getByPlaceholderText('Name');
    const descriptionInput = getByPlaceholderText('Description');
    const addButton = getByText('Add Data');

    fireEvent.changeText(nameInput, 'New Name');
    fireEvent.changeText(descriptionInput, 'New Description');
    fireEvent.press(addButton);

    await waitFor(() => {
      expect(getByText('New Name')).toBeTruthy();
      expect(getByText('New Description')).toBeTruthy();
    });
  });
});
```

### Updating the `HomeScreen` Component

Add test identifiers to the `HomeScreen.tsx` component to make it easier to select elements during tests:

```typescript
// src/HomeScreen.tsx

import React, { useState } from 'react';
import { View, Text, TextInput, Button, ActivityIndicator, FlatList } from 'react-native';
import { useQuery, useMutation } from '@apollo/client';
import { GET_DATA } from './queries';
import { ADD_DATA } from './mutations';

interface DataItem {
  id: string;
  name: string;
  description: string;
}

interface GetDataResult {
  data: DataItem[];
}

interface AddDataVars {
  name: string;
  description: string;
}

const HomeScreen: React.FC = () => {
  const { loading, error, data } = useQuery<GetDataResult>(GET_DATA);
  const [addData] = useMutation<DataItem, AddDataVars>(ADD_DATA);
  
  const [name, setName] = useState('');
  const [description, setDescription] = useState('');

  const handleAddData = () => {
    addData({ 
      variables: { name, description },
      refetchQueries: [{ query: GET_DATA }]
    });
  };

  if (loading) return <ActivityIndicator testID="loading" size="large" color="#0000ff" />;
  if (error) return <Text>Error: {error.message}</Text>;

  return (
    <View>
      <TextInput 
        placeholder="Name"
        value={name}
        onChangeText={setName}
        testID="name-input"
      />
      <TextInput 
        placeholder="Description"
        value={description}
        onChangeText={setDescription}
        testID="description-input"
      />
      <Button title="Add Data" onPress={handleAddData} testID="add-button" />
      
      <FlatList
        data={data?.data}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => (
          <View>
            <Text>{item.name}</Text>
            <Text>{item.description}</Text>
          </View>
        )}
      />
    </View>
  );
};

export default HomeScreen;
```

### Running the Tests

Now, you can run the tests using the command:

```bash
npm test
```

### Conclusion

Adding tests to your React Native project with GraphQL and TypeScript is a crucial step to ensure the quality and robustness of your application. With the provided configurations and examples, you are well on your way to creating a comprehensive test suite for your application.