How to Get Started with React Query - Migrate from Fetch

Author

Spencer Carli

Developer, cat dad, and devout pizza lover. Teaching at React Native School and building apps with Handlebar Labs.

Last Updated: April 28, 2021

Fetching data from a remote source is a must in just about every app you're going to build. Unfortunately, fetching data is rarely as simple as just fetching the data and rendering it. A few things to consider:

  • What do we do when we're waiting for the data to load?
  • What if an error occurs?
  • How do I keep my data up to data?
  • What if my user has a poor/slow internet connection?

These are all things that React Query can help you with.

Today we'll be migrating a basic demo app from using the fetch API to using React Query.

The final code can be viewed on Github.

App Overview

Let's take a look at our app. We can request a list of posts and then display them. When tapping on a post title we navigate to a new screen where we can also see the comments, which come from a separate request.

01

Right now that data is fetched via the fetch API.

// App/screens/List.js

import React from 'react';
import { View, StyleSheet, FlatList, TouchableOpacity } from 'react-native';

import { Text } from '../components/Text';
import colors from '../constants/colors';

const styles = StyleSheet.create({
  /// ...
});

export const List = ({ navigation }) => {
  const [data, setData] = React.useState([]);

  React.useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/posts')
      .then((res) => res.json())
      .then((response) => {
        setData(response);
      });
  }, []);

  return (
    <FlatList
      data={data}
      style={styles.container}
      renderItem={({ item }) => (
        <TouchableOpacity
          onPress={() => navigation.push('Post', { post: item })}
        >
          <View key={item.id} style={styles.item}>
            <Text>{item.title}</Text>
          </View>
        </TouchableOpacity>
      )}
    />
  );
};
// App/screens/Post.js
import React from 'react';
import { View, StyleSheet, ScrollView } from 'react-native';

import { Text } from '../components/Text';
import colors from '../constants/colors';

const styles = StyleSheet.create({
  ...
});

export const Post = ({ route }) => {
  const post = route?.params?.post;
  const [comments, setComments] = React.useState([]);

  React.useEffect(() => {
    fetch(`https://jsonplaceholder.typicode.com/comments?postId=${post.id}`)
      .then(res => res.json())
      .then(response => {
        setComments(response);
      });
  }, []);

  return (
    <ScrollView style={styles.container}>
      <Text type="header">{post.title}</Text>
      <Text>{post.body}</Text>
      {comments && comments.length > 0 && (
        <>
          <Text type="header" style={{ marginTop: 20 }}>
            Comments
          </Text>
          {comments.map(comment => (
            <View key={comment.id} style={styles.item}>
              <Text>{comment.body}</Text>
              <Text>{comment.email}</Text>
            </View>
          ))}
        </>
      )}
    </ScrollView>
  );
};

Let's fetch it with React Query instead.

Configuring React Query

One of the great things about React Query is that it's just one dependency. If you know me you know I like to minimize dependencies.

And, since it's purely JS, you only need to worry about installing it from NPM.

npm install --save react-query

Once that's complete we need to set up the QueryClient and QueryClientProvider. These are used to manage the "magic" that React Query provides us (such as caching).

So, at the root of my project, I'm going to set up the provider:

// App/index.js

import React from 'react';
import { StatusBar } from 'expo-status-bar';
import { NavigationContainer } from '@react-navigation/native';
import { QueryClient, QueryClientProvider } from 'react-query';

import { Main } from './navigation/Main';

const queryClient = new QueryClient();

export default function App() {
  return (
    <>
      <StatusBar style="auto" />
      <NavigationContainer>
        <QueryClientProvider client={queryClient}>
          <Main />
        </QueryClientProvider>
      </NavigationContainer>
    </>
  );
}

Posts Query

With that done let's migrate over the request that grabs all of the posts.

// App/screens/List.js

import React from 'react';
import { View, StyleSheet, FlatList, TouchableOpacity } from 'react-native';
import { useQuery } from 'react-query';

// ...

export const List = ({ navigation }) => {
  /*
  const [data, setData] = React.useState([]);

  React.useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/posts')
      .then(res => res.json())
      .then(response => {
        setData(response);
      });
  }, []);
  */
  const { data } = useQuery('posts', () =>
    fetch('https://jsonplaceholder.typicode.com/posts').then((res) =>
      res.json()
    )
  );

  // ...
};

You can see the difference there (unchanged code is removed for brevity).

The code is a little bit more succinct but that's not the main benefit of React Query.

As you can see with useQuery the first argument is a simple string. This caches and tracks the result of that query.

It's important to note that the key needs to be unique to that request (to https://jsonplaceholder.typicode.com/posts).

The second argument is how we actually get the data. It either needs to resolve data or throw an error.

But still, the caching isn't the only benefit here...

The initial fetch implementation was super basic. We don't manage:

  • request states (loading, fetching, error)
  • errors

Let alone refetching data or caching it.

And guess what! That's all stuff that React Query gives you for free.

If we take a look at the object returned from userQuery we can see all that is available to us

Comments Query

The comments query is a little bit more complex... Let's get it set up.

// App/screens/Post.js

import React from 'react';
import { View, StyleSheet, ScrollView } from 'react-native';
import { useQuery } from 'react-query';

// ...

export const Post = ({ route }) => {
  const post = route?.params?.post;
  /*
  React.useEffect(() => {
    fetch(`https://jsonplaceholder.typicode.com/comments?postId=${post.id}`)
      .then(res => res.json())
      .then(response => {
        setComments(response);
      });
  }, []);
  */

  const { data: comments } = useQuery(['comments', post.id], () =>
    fetch(
      `https://jsonplaceholder.typicode.com/comments?postId=${post.id}`,
    ).then(res => res.json()),
  );

  return ...;
};

Syntax is a bit different here... why? Well the query key (the first argument to useQuery) can be anything serializable (string, array, object). So in this case we're creating a key that will cache comments per post (rather than globally).

Let's try this out... I'm going to turn on Network Link Conditioner (which you can learn more about in our post about progressive image loading) so I have a painfully slow connection.

Watch the gif below... what do you see?

https://media.giphy.com/media/U1iqYl5z0mmuOMUopa/giphy.gif

When we make our first request to a post's comments the request is slooow. But when we make our second request? Instant!

Why?

Because we're not making a request - we're using cached data. Caching we get for free from React Query.

We just barely scratched the surface of React Query today but I'm finding it to be a powerful library that handles a lot of the simple, and not so simple, things that need to be done when making remote data requests.

Be sure to dive into the official docs. I'm struggling to find a reason why not to use this library.

Questions/comments/concerns? Let us know on Twitter.

Final code.

React Native School Logo

React Native School

Want to further level up as a React Native developer? Join React Native School! You'll get access to all of our classes and our private Slack community.