Introduction
In this article, I’ll teach you how to use RTK Query to fetch and post data in a React Native Expo TypeScript project. We’ll use the JSONPlaceholder API as an example, which provides a REST API for testing and prototyping. At the end of this article, you’ll have a good understanding of how RTK Query works and how to use it to handle API calls in your own React Native Expo apps. Now let’s get started!
Set up a new Expo TypeScript app with RTK Query
Let’s create a new Expo React-Native typescript project with the command below:
Select a blank typescript project when the options appear
npx create-expo-app --template
Install Redux Toolkit
yarn add @reduxjs/toolkit
Run the project on the emulator of your choice
yarn start
Creating API endpoints with RTK-Query
- Import createApi and fetchBaseQuery from the ‘@reduxjs/toolkit/query/react’
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
2. Define a new constant named “api” that uses the createApi function to create a new API instance. The createApi function takes an object as its argument with three properties:
- reducerPath: A string that specifies the name of the slice of the Redux store where the API state will be stored.
- baseQuery: A function that returns a base configuration object used for every request. The fetchBaseQuery function is a utility function that creates a default base query object with a baseUrl property.
- endpoints: A function that takes a builder object and returns an object containing all the query and mutation endpoints for your API. In this example, it defines two endpoints: getUsers and createUser. builder.query is a way to define a query endpoint with RTK Query. You define the endpoint’s URL and HTTP method, and RTK Query generates a hook function that you can use to call the endpoint and get the data.
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: 'https://jsonplaceholder.typicode.com' }),
endpoints: (builder) => ({
getUsers: builder.query({
query: () => ({
url: '/users/',
method: 'Get',
}),
}),
createUser: builder.mutation({
query: (body) => ({
url: '/users',
method: 'POST',
body,
}),
}),
}),
});
3. Export two hooks, useGetUsersQuery and useCreateUserMutation, from the api object created in step 2. These hooks are generated automatically by RTK Query based on the endpoints defined in the endpoints property. You can use these hooks to call the getUsers and createUser endpoints respectively.
export const {useGetUsersQuery,useCreateUserMutation} = api
With this API slice set up, we can now use RTK Query to fetch and submit data from our React Native Expo app.
will use ApiProvider to wrap the components in App.tsx; see the code snippet below:
import { ApiProvider } from '@reduxjs/toolkit/dist/query/react';
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View } from 'react-native';
import { api } from './apiSlice';
import { UserList } from './GetUsers';
function AppWrapper() {
return (
<View style={styles.container}>
<StatusBar style="auto" />
<UserList />
</View>
);
}
export default function App(){
return (
<ApiProvider api={api}>
<AppWrapper/>
</ApiProvider>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
If you app has got a redux store, Add the apiSlice.reducer
to your store:
import { configureStore } from '@reduxjs/toolkit';
import { api } from './apiSlice';
import { setupListeners } from '@reduxjs/toolkit/query';
export const store = configureStore({
reducer: {
api: apiSlice.reducer,
// Add your other reducers here
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false,
}).concat(api.middleware),
setupListeners(store.dispatch);
export default store;
The setupListeners function listens for certain events that happen when you use RTK-Query, and it needs access to the “dispatch” method of your Redux store to work. It’s like setting up a pair of ears to hear what’s going on with RTK-Query.
To use the setupListeners function, you can add it to your code alongside the createApi function in the apiSlice file. For example, you can tell your app to automatically fetch new data when the app regains focus or reconnects to the internet by adding “refetchOnFocus: true” and “refetchOnReconnect: true” to the createApi function. see the code below for context
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: 'https://jsonplaceholder.typicode.com' }),
refetchOnFocus: true,
refetchOnReconnect: true,
endpoints: (builder) => ({
getUsers: builder.query({
query: () => ({
url: '/users/',
method: 'Get',
}),
}),
createUser: builder.mutation({
query: (body) => ({
url: '/users',
method: 'POST',
body,
}),
}),
}),
});
Automatic Re-fetching
RTK-Query has a feature called “cache tag” system, which automatically re-fetches query endpoints that have data that could be affected by mutation endpoints. This means that you can set up your API so that firing a particular mutation will cause a specific query endpoint to consider its cached data invalid. Then, it will re-fetch the data if there is an active subscription. This helps ensure that your app always displays the most up-to-date information.
Tags
RTK-Query’s tagTypes help group query and mutation results under a specific tag. For example, if you have a query and a mutation related to “users”, you can group them under a tag called “users”. This way, you can easily invalidate or refetch all queries/mutations related to “users” at once if necessary.
tagTypes: ['users']
Provide Tags
providesTags is a function that returns an array of tags for a specific query or mutation result. This is used to associate a query/mutation result with one or more tags. For instance, if you have a query that fetches a single user by ID, you can associate the result with the “users” tag using providesTags.
providesTags: ['users']
Invalidate Tags
invalidatesTags is a function that returns an array of tags that should be invalidated when a specific query or mutation is executed. This is used to specify which queries/mutations should be invalidated when a particular action is taken. For example, if you have a mutation that adds a new user, you can invalidate all queries/mutations related to “users” using invalidatesTags.
invalidatesTags: ['users']
Here’s an example of how to use these features in RTK-Query:
const api = createApi({
tagTypes: ['users'], // Declare a tag type to group related queries/mutations
endpoints: (builder) => ({
getUsers: builder.query({
query: () => '/users',
providesTags: ['users'], // associate the result of this mutation with the "users" tag
}),
createUser: builder.mutation({
query: (data) => ({
url: '/users',
method: 'POST',
body: data,
}),
invalidatesTags: ['users'], // invalidate all queries/mutations with the "users" tag when this mutation is executed
}),
}),
});
Fetching data using RTK-Query
Now that we have our API slice set up, let’s use it to fetch data from JSONPlaceholder in our React Native Expo app.
- Import necessary React Native components and the
useGetUsersQuery
hook fromapiSlice
.
import React from 'react';
import {
Text,
FlatList,
ActivityIndicator,
StyleSheet,
View,
SafeAreaView
} from 'react-native';
import { useGetUsersQuery } from './apiSlice';
2. Define a functional component called UserList
.
export const UserList = () => {}
3. Call the useGetUsersQuery
hook to fetch data and track loading state.
const { data, isLoading } = useGetUsersQuery();
4. Check if data is still loading, and display an activity indicator if it is.
if (isLoading) {
return <ActivityIndicator />;
}
5. If data has loaded, render a FlatList
component that displays the user data.
return (
<SafeAreaView>
<FlatList
data={data}
keyExtractor={(item) => item.id.toString()}
renderItem={({ item }) => (
<View style={styles.itemContainer}>
<Text>id: {item.id}</Text>
<Text>name: {item.name}</Text>
<Text>phone: {item.phone}</Text>
<Text>email: {item.email}</Text>
</View>)}
/>
</SafeAreaView>
);
6. Define the styles for the UserList
component.
const styles = StyleSheet.create({
container: {
flex: 1,
},
itemContainer: {
backgroundColor: '#fff',
padding: 5,
paddingHorizontal: 18,
marginTop: 5,
borderWidth: 1,
borderRadius: 10,
alignItems: 'center',
gap: 2
},
});
Below is the entire code put together
import React from 'react';
//import { useGetTodosQuery } from './apiSlice';
import {
Text,
FlatList,
ActivityIndicator,
StyleSheet,
View,
SafeAreaView
} from 'react-native';
import { useGetUsersQuery } from './apiSlice';
export const UserList = () => {
const { data, isLoading } = useGetUsersQuery();
if (isLoading) {
return <ActivityIndicator />;
}
return (
<SafeAreaView>
<FlatList
data={data}
keyExtractor={(item) => item.id.toString()}
renderItem={({ item }) => (
<View style={styles.itemContainer}>
<Text>id: {item.id}</Text>
<Text>name: {item.name}</Text>
<Text>phone: {item.phone}</Text>
<Text>email: {item.email}</Text>
</View>)}
/>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
itemContainer: {
backgroundColor: '#fff',
padding: 5,
paddingHorizontal: 18,
marginTop: 5,
borderWidth: 1,
borderRadius: 10,
alignItems: 'center',
gap: 2
},
});
Here’s the output:
This is just a basic example, but RTK Query provides many more features for fetching data, including caching, polling, and optimistic updates. Be sure to check out the RTK Query documentation for more information.
Now let’s move on to using RTK Query to submit data.
Posting data using RTK-Query
Posting data with RTK Query is just as easy as fetching it. Let’s create a generic modal, buttons and a user form.
- Import necessary React Native components and the useCreateUserMutation hook from
apiSlice
.
import React, { useState } from 'react';
import {
Modal,
TextInput,
Button,
StyleSheet,
View,
Alert } from 'react-native';
import { useCreateUserMutation } from './apiSlice';
2. Define a functional component called CreateUser. It takes two props, visible
and onClose
, which we will use to control the visibility of the modal and to close the modal respectively.
export const CreateUser = ({ visible, onClose }) => {
// component logic here
};
3. We define some state using the useState
hook. The useState
hook returns an array containing two values: the current value of the state and a function to update the state. For example, setName
is a function that updates the name
state when called.
const [name, setName] = useState('');
const [phone, setPhone] = useState('');
const [email, setEmail] = useState('');
const [id, setId] = useState('');
const [address, setAddress] = useState('');
const [website, setWebsite] = useState('');
const [company, setCompany] = useState('');
4. Next, we have a line that uses the useCreateUserMutation
hook we imported earlier:
const [createUser] = useCreateUserMutation()
This line calls the useCreateUserMutation
hook and assigns the first element of the returned array to a constant named createUser
. We can use this function to send a request to create a new user using our API.
5. We define a function named handleSubmit
which handles form submission
const handleSubmit = () => {
const formData = {
name,
phone,
email,
id
}
createUser(formData)
.unwrap()
.then(() => {
Alert.alert("User created");
setName('');
setPhone('')
setEmail('')
setId('')
setAddress('')
setCompany('')
setWebsite('')
})
.catch(() =>
Alert.alert("there was an error"),
);
onClose();
setName('');
setPhone('')
setEmail('')
setId('')
setAddress('')
setCompany('')
setWebsite('')
};
This code creates a function that is called when the user submits a form. It gets the values of the form and sends them to the server to create a new user. If there are no errors, an alert message appears saying that the user was created. If there are errors, another alert message appears saying that there was an error. The function then closes the form and resets the values of the form fields.
The code uses a special function called “unwrap” to handle the response/error from the server inline. If the response is successful, the “then” block is executed and the user is notified that the user was created. If there is an error, the “catch” block is executed and the user is notified that there was an error. This makes it easier for developers to handle responses from the server in a concise and readable way.
6. Return a form in modal
return (
<Modal visible={visible} animationType="slide">
<View style={styles.modalContainer}>
<TextInput
placeholder="Id"
value={id}
onChangeText={setId}
style={styles.input}
/>
<TextInput
placeholder="Name"
value={name}
onChangeText={setName}
style={styles.input}
/>
<TextInput
placeholder="Phone"
value={phone}
onChangeText={setPhone}
style={styles.input}
/>
<TextInput
placeholder="Email"
value={email}
onChangeText={setEmail}
style={styles.input}
/>
<TextInput
placeholder="Address"
value={address}
onChangeText={setAddress}
style={styles.input}
/>
<TextInput
placeholder="Website"
value={website}
onChangeText={setWebsite}
style={styles.input}
/>
<TextInput
placeholder="Company"
value={company}
onChangeText={setCompany}
style={styles.input}
/>
<Button title="Submit" onPress={handleSubmit} />
<Button title="Cancel" onPress={onClose} />
</View>
</Modal>
);
Below is the complete code for CreatUser component
import React, { useState } from 'react';
import { Modal, TextInput, Button, StyleSheet, View, Alert } from 'react-native';
import { useCreateUserMutation } from './apiSlice';
export const CreateUser = ({ visible, onClose }) => {
const [name, setName] = useState('');
const [phone, setPhone] = useState('');
const [email, setEmail] = useState('');
const [id, setId] = useState('');
const [address, setAddress] = useState('');
const [website, setWebsite] = useState('');
const [company, setCompany] = useState('');
const [createUser] = useCreateUserMutation()
const handleSubmit = () => {
const formData = {
name,
phone,
email,
id
}
createUser(formData)
.unwrap()
.then(() => {
Alert.alert("User created");
setName('');
setPhone('')
setEmail('')
setId('')
setAddress('')
setCompany('')
setWebsite('')
})
.catch(() =>
Alert.alert("there was an error"),
);
onClose();
setName('');
setPhone('')
setEmail('')
setId('')
setAddress('')
setCompany('')
setWebsite('')
};
return (
<Modal visible={visible} animationType="slide">
<View style={styles.modalContainer}>
<TextInput
placeholder="Id"
value={id}
onChangeText={setId}
style={styles.input}
/>
<TextInput
placeholder="Name"
value={name}
onChangeText={setName}
style={styles.input}
/>
<TextInput
placeholder="Phone"
value={phone}
onChangeText={setPhone}
style={styles.input}
/>
<TextInput
placeholder="Email"
value={email}
onChangeText={setEmail}
style={styles.input}
/>
<TextInput
placeholder="Address"
value={address}
onChangeText={setAddress}
style={styles.input}
/>
<TextInput
placeholder="Website"
value={website}
onChangeText={setWebsite}
style={styles.input}
/>
<TextInput
placeholder="Company"
value={company}
onChangeText={setCompany}
style={styles.input}
/>
<Button title="Submit" onPress={handleSubmit} />
<Button title="Cancel" onPress={onClose} />
</View>
</Modal>
);
};
const styles = StyleSheet.create({
modalContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
input: {
borderWidth: 1,
borderRadius: 5,
padding: 10,
marginVertical: 5,
width: '80%',
},
});
That’s it! With just a few lines of code, we can now fetch and submit data to our API using RTK Query in React native expo App.
Conclusion
I hope you found this helpful! Here is the git repo, If you have any questions or feedback, leave a comment below.