How to use SWR in React Native - Fetch and Cache data

Vadim Savin profile picture
Vadim SavinApr 21, 2024

SWR is a simple yet powerful data fetching and caching library developed by Vercel - the team behind NextJS.

In this tutorial, we will explore the power of SWR in a React Native (Expo) application. We will cover the fundamentals of SWR, how to get started with it and how to fetch data from both a REST API and a GraphQL API.

Create a new Expo project

Let’s start by creating a brand new Expo project.

BASH
npx create-expo-app@latest Swr

Open the new project in Visual Studio Code.

Before we run the project, let’s rename App.js to App.tsx to use Typescript.

To run the project, open a new terminal and run npm start.

Expo will detect that we have a typescript file, and will install and configure typescript for us. Just press enter.

Untitled.png

When the development server starts, we should see the expo development menu. From here, we can scan the QR using the Expo Go app with a physical device (Android or iOS). We can also run it on an iOS simulator by pressing "i", or on an Android emulator by pressing "a".

Get started with SWR

Install SWR

Let’s install the SWR library, by opening a new terminal, and running:

BASH
npm i swr

Fetch data

Let’s use SWR to fetch our first data. We will use the JSON Placeholder API and fetch a list of posts. Open App.tsx, and let’s implement the first request.

JAVASCRIPT
import useSWR from 'swr';
const fetcher = (...args) => fetch(...args).then((res) => res.json());
export default function App() {
const { data, error, isLoading } = useSWR(
'https://jsonplaceholder.typicode.com/posts',
fetcher
);
if (isLoading) {
return <ActivityIndicator />
}
if (error) {
return <Text>{error.message}</Text>
}
console.log(JSON.stringify(data, null, 2));
return (...)
}

With this in place, if we refresh our app, we will see a spinner for a split of a second, while the data is loading, and then it should switch to the default UI. Open up the expo terminal, and check the logs. We should see a list of posts that we received from the API console logged.

Render posts

Let’s build a new component inside src/components/PostListItem.tsx that will render one post

JAVASCRIPT
import { View, Text, StyleSheet } from 'react-native';
const PostListItem = ({ post }) => {
return (
<View style={styles.container}>
<Text style={styles.title}>{post.title}</Text>
<Text style={styles.body}>{post.body}</Text>
</View>
);
};
const styles = StyleSheet.create({
container: {
backgroundColor: '#fff',
padding: 10,
gap: 10,
borderRadius: 10,
},
title: {
fontWeight: 'bold',
fontSize: 20,
},
body: {
color: 'gray',
},
});
export default PostListItem;

Now, let’s render the posts in a <FlatList /> inside App.tsx

JAVASCRIPT
<FlatList
data={data}
contentContainerStyle={{ gap: 10, padding: 10 }}
renderItem={({ item }) => <PostListItem post={item} />}
/>

Let’s make data fetching reusable

Let’s first extract the fetcher function to separate file src/utils/fetcher.ts because we will use it in multiple files.

JAVASCRIPT
export const fetcher = (...args) => fetch(...args).then((res) => res.json());

Now, let’s create a custom hook usePosts inside src/hooks/posts.tsthat will fetch the posts and will return an object with 3 fields: posts, isLoading, and error.

JAVASCRIPT
import { fetcher } from '../utils/fetcher';
import useSWR from 'swr';
export function usePosts() {
const { data, error, isLoading } = useSWR(
'https://jsonplaceholder.typicode.com/posts',
fetcher
);
return {
posts: data,
isLoading,
error,
};
}

Now, we can use this re-usable hook inside App.tsx

JAVASCRIPT
const { posts, error, isLoading } = usePosts();

Make sure to replace data={data} with data={posts} inside the FlatList.

Fetch user data for every post

Let’s start by creating a custom hook useUser inside src/hooks/users.ts . The hook will receive the user id and will return the user, isLoading, and error.

JAVASCRIPT
import { fetcher } from '../utils/fetcher';
import useSWR from 'swr';
export function useUser(id: number) {
const { data, error, isLoading } = useSWR(
`https://jsonplaceholder.typicode.com/users?id=${id}`,
fetcher
);
return {
user: data?.[0],
isLoading,
error,
};
}

Having this hook, we can fetch the user details inside the PostListItem.tsx component

JAVASCRIPT
const { user } = useUser(post.userId);

Render the user name, above the title of the post

JAVASCRIPT
<Text>{user?.name}</Text>

Now we can really see the power of SWR. Even though we have 100 posts in the list, and we are fetching the user data for every post, we would imagine that we have 100 requests to the API for user details. In fact, we only have 5 requests, because there are 5 unique users. That’s because SWR automatically deduped, cached and shared the data automatically.

To see that, open the debugger by pressing "J" in your expo terminal, go to the network tab and refresh the app.

Untitled.png

Posts Screen

Before we move forward, let’s move the logic of the posts screen (fetching, and rendering the FlatList) to a separate file src/app/PostsScreen.tsx.

Import this component, and render it inside App.tsx

For more details, check out this commit.

SWR Global configuration

So far, we have used useSWR hook, without any global configurations. That’s great to get started quickly.

When want to provide global configurations, that will be reused throughout all SWR requests, we can do that using the <SWRConfig> component wrapped around our whole app.

One useful configuration we can setup at this moment, is the fetcher function. Let’s set it up inside App.tsx

JAVASCRIPT
import { SWRConfig } from 'swr';
import { fetcher } from './src/utils/fetcher';
export default function App() {
return (
<SWRConfig value={{ fetcher, dedupingInterval: 2000 }}>
...
</SWRConfig>
)
}

With this global config, we can remove the fetcher from individual useSWR hooks (hooks/posts.ts and hooks/users.ts), making them even cleaner:

JAVASCRIPT
const { data, isLoading, error } = useSWR(
'https://jsonplaceholder.typicode.com/posts',
// fetcher - no longer needed. The global fetcher will be used
);

Mutations & Revalidation

SWR provides the mutate and useSWRMutation APIs for mutating remote data and related cache. We are going to cover both scenarios

  1. Revalidating data from the cache (ex: we pull down the flat list, we want to revalidate the posts query)
  2. Mutating remote data: creating/updating a post on the server, and updating it in the cache

Prepare the UI

Let’s first create some buttons in our PostsScreen.tsx

JAVASCRIPT
const refresh = () => {
// refresh posts
}
const onCreatePost = () => {
// create a new post
}
return (
<>
<Button title="Refresh" onPress={refresh} />
<Button title="Create post" onPress={onCreatePost} />
<FlatList
...
/>
</>
);

Revalidate posts

To revalidate and refresh the posts, we should invalidate the query that fetched the posts. We do that, by running a mutation using the same key, as we used for fetching the posts. In the hooks/posts.tsx, the first argument of the useSWR hook serves both as the URL to be fetched, but more importantly, it serves as the key that SWR is using to cache the response.

JAVASCRIPT
const { data, isLoading, error } = useSWR(
'https://jsonplaceholder.typicode.com/posts',
);

Now, we have to run a mutation, using the same key to revalidate the data and fetch it again from the server. There are 2 ways to do it: using the global mutate, and using a mutate returned by useSWR hook. Let’s see the difference.

Global mutate

The global mutate, allows us to invalidate cached data by providing a key.

JAVASCRIPT
import { useSWRConfig } from "swr"
export default function PostsScreen() {
const { mutate } = useSWRConfig()
const refresh = () => {
// refresh posts
mutate('https://jsonplaceholder.typicode.com/posts');
}
}

If we only provide the key to the mutation, it will simply revalidate any active useSWR hook that is using the same key.

We can also provide new data and specify options when calling mutate:

JAVASCRIPT
mutate(key, data, options)

Bound mutate

The bound mutate is returned by the useSWR hook, and is basically a shorthand to the global mutate, that does not need a key. The key it uses is the same key as the useSWR request. This is handy to mutate data for an existing query.

To implement this, let’s first re-export the mutate field from our usePosts hook from hooks/posts.tsx

JAVASCRIPT
export function usePosts() {
const { data, isLoading, error, mutate } = useSWR(
'https://jsonplaceholder.typicode.com/posts'
);
return {
posts: data,
isLoading,
error,
mutate,
};
}

Now, we can access this bound mutate function from PostsScreen and we can call it without any argument, and it will revalidate the posts request

JAVASCRIPT
export default function PostsScreen() {
const { posts, isLoading, error, mutate } = usePosts();
const refresh = () => {
// refresh posts
mutate();
}
}

Mutating remote data

Let’s have a look at how we can mutate remote data to create a new post.

SWR provides useSWRMutation as a hook for remote mutations. The remote mutations are only triggered manually, instead of automatically like useSWR.

Let’s start by creating a function createPost that will send the post request to our API, and a custom hook useCreatePost that will serve as a shorthand for useSWRMutation. We will do this inside hooks/posts.ts

JAVASCRIPT
const createPost = async (url, newPost: NewPost) => {
const response = await fetch(url, {
method: 'POST',
body: JSON.stringify(newPost),
headers: {
'Content-type': 'application/json; charset=UTF-8',
},
});
return response.json();
};
export const useCreatePost = () => {
const { trigger, data, isMutating, error } = useSWRMutation(
'https://jsonplaceholder.typicode.com/posts',
createPost
);
return {
trigger,
newPost: data,
isMutating,
error,
};
};

Now we can use the custom hook useCreatePost inside the PostsScreen to define the mutation, and then trigger it when we press the Create Post button

JAVASCRIPT
export default function PostsScreen() {
const { trigger, newPost } = useCreatePost();
const onCreatePost = async () => {
const newPost = {
title: 'New Post',
body: 'This is a new post created from the app',
userId: 1,
};
try {
await trigger(newPost);
} catch (e) {
Alert.alert('Failed to create the post');
}
};
...
}

With this in place, we can try to press the "Create post" button. We will see the new post for a split second, and then it will disappear. Why?

Well, that’s actually intended behaviour. First of all, the API we are using is not actually saving the post. It just simulated the request.

And, because both our fetch posts and create post SWR requests are using the exact same key https://jsonplaceholder.typicode.com/posts means that running the mutation, will also revalidate the posts, and fetch them again from the server. That’s why the post disappears.

We can also disable this behavior through the options when calling the trigger function. Simply set revalidate: false and the mutation will not revalidate the cache.

JAVASCRIPT
await trigger(newPost, { revalidate: false });

Optimistic updates

Another option we can provide to a mutation, is the optimisticData. Using it, we can make our UI feel really fast, by updating the local cache instantly, without waiting the request to be processed by the API.

JAVASCRIPT
await trigger(newPost, {
revalidate: false,
optimisticData: (current) => {
return [newPost, ...current];
},
});

GraphQL Requests

SWR is backend agnostic. It means that it doesn’t matter if we are working with a REST API or a GraphQL API.

Let’s use IBM StepZen to set up a simple GraphQL API and fetch it inside our app.

Setup the GraphQL API

Install the StepZen cli:

BASH
npm install -g stepzen

Create a free StepZen account.

Now, let’s log in with StepZen cli by running stepzen login and providing your account name and Admin Key, which you can find on your StepZen Dashboard.

Now, let’s create a new folder stepzen inside our React Native project, and navigate to it

BASH
mkdir stepzen && cd stepzen

The next step is to convert the https://jsonplaceholder.typicode.com/posts endpoint to GraphQL by simply running:

BASH
stepzen import curl https://jsonplaceholder.typicode.com/posts

Now we can run stepzen start to deploy and run our new GraphQL API. After a couple of settings, you should see the URL in your terminal under "Explore it with GraphiQL at:". Click that URL, and it will open a graphical interface to test your new GraphQL endpoint.

Now, that our GraphQL API is ready, let’s see how we can fetch it with SWR

SWR with GraphQL

To send GraphQL requests, we can use the library graphql-request. Install it in our React native app by running the following command in the root directory of your project.

BASH
npm install graphql-request

Now, let’s create a new gqlFetcher function in utils/fetcher.ts that will help us send requests using this library

JAVASCRIPT
import { request } from 'graphql-request';
export const gqlFetcher = (query) =>
request({
url: 'https://<YOUR_ACCOUNT_NAME>.stepzen.net/api/<YOUR_API_NAME>/__graphql',
document: query,
requestHeaders: {
Authorization:
'Apikey <YOUR_API_KEY>',
},
});

Make sure to replace <YOUR_ACCOUNT_NAME> , <YOUR_API_NAME> and <YOUR_API_KEY> with the values from your StepZen Dashboard.

Now, we can use this GraphQL fetcher in a new custom hook inside hooks/posts.ts

JAVASCRIPT
export function useGqlPosts() {
const query = `
query myQuery {
myQuery {
title
body
userId
}
}
`;
const { data, isLoading, error, mutate } = useSWR(query, gqlFetcher);
return {
posts: data?.myQuery,
isLoading,
error,
mutate,
};
}

You can now replace usePosts with useGqlPosts hook inside the PostsScreen to use the new graphql request. To test if everything is working, you can go back to the query inside useGqlPosts and remove some fields. You should see this reflected in the UI.

That’s all it takes to fetch a GraphQL API using SWR.

SWR and React Native

SWR is designed to work out of the box in web environments. So far, everything worked fine, but there are some small but important things that are not working out of the box in React Native.

For example, SWR will not revalidate your queries when the app goes from background to active. It will not be revalidated when the phone connects to the internet.

To enable this features, we have to manually provide this functionality through the SWR Configuration.

Read more about this in the SWR React Native docs.

There is also a community library swr-react-native built by Fernando Rojo. It’s a simple drop-in replacement of SWR, customized for React Native.

Source code

If you encounter any issues, check out the full source code of this project.


Vadim Savin profile picture

Vadim Savin

Hi đź‘‹ Let me introduce myself

I started my career as a Fullstack Developer when I was 16 y.o.

In search of more freedom, I transitioned to freelancing, which quickly grew into a global software development agency 🔥

Because that was not challenging enough, I started my startup which is used by over 20k users. This experience gave another meaning to being a (notJust) developer 🚀

I am also a proud ex-Amazon SDE and Certified AWS Architect, Developer and SysOps. You are in good hands đź‘Ś


Read next