Lawrence Eagles Senior full-stack developer, writer, and instructor.

Using React Query for state management in React Native

7 min read 1996

Using React Query for state management in React Native

Introduction

State management is an integral part of React Native development.

A state is a JavaScript object used to handle mutable React Native data in a component.
In a React Native component, a state may be mutated across time, mostly as a result of user events.

A simple local state in React Native can be managed with the setState method or the useState hook.

However, as our app grows bigger, you may need a state management library because it becomes increasingly difficult to manage state shared across components.

Data-fetching libraries for React Native

So, state management is one of the common issues you can encounter when developing a React application. And it can be a difficult aspect to master because there are tons of state management libraries in React.

Below are some of the popular ones:

Context

Data flows unilaterally in React, from top to bottom — parent component to child component. The Context API provides a way to pass data through the component tree without prop drilling.

Redux

Redux is a framework-agnostic state management layer. It is very powerful and great for big applications. You can use Redux with React, React Native, Vue, etc.

Redux offers a single global state object called the store, and it uses several concepts such as actions, action creators, and reducers. In most cases, you would be using Redux with a middleware such as Redux Thunk or Redux-Saga, and this introduces more complexity because you have more things to learn. However, Redux is great for client-side state management.

You can get a comprehensive guide to using Redux and React Native here.

React Query

This article is focused on state management in React Native using React Query, a technology that describes itself as the missing data-fetching library for React.

Let’s learn more about React Query in the next section.

What is React Query 3?

React Query is a light caching layer that lives in our application. As a data-fetching library, it is agnostic to how we fetch our data. The only thing React Query needs to know is the promise returned by Axios or Fetch.

The two main concepts of React Query are queries and mutations. While queries deal with fetching data, mutations handle modifying data on the server.

React Query exports a useQuery hook for handling queries. The useQuery hook takes two parameters. The first parameter is a unique identifier to describe what we are fetching. And the second identifier is the fetcher function — an async function responsible for either getting your data or throwing an error.

Consider the code below:

import { useQuery } from 'react-query';
import axios from 'axios';

const fetchPosts = async () => {
    const { data } = await axios.get('https://jsonplaceholder.typicode.com/posts');
    return data;
};

const usePosts = () => useQuery('posts', fetchPosts);

The useQuery hook returns a query object and also handles all the lifecycle around our fetching so we don’t need to use useEffect.



The query object consists of some important states mentioned by React Query’s docs:

  • isLoading or status === 'loading' — The query has no data and is currently fetching
  • isError or status === 'error' — The query encountered an error
  • isSuccess or status === 'success' — The query was successful and data is available
  • isIdle or status === 'idle' — The query is currently disabled (you’ll learn more about this in a bit)

A query can only be in one of these states. And beyond these primary states, it also contains more information such as:

  • error — If the query is in an isError state, the error is available via the error property
  • data — If the query is in a success state, the data is available via the data property
  • isFetching — In any state, if the query is fetching at any time (including background refetching), isFetching will be true

Without React Query, each of these states might have been handled using a different useState hook. So we can see that React Query helps us reduce our boilerplate. This is a gain in developer experience — DX gain.

Also, React Query helps with caching out of the box. And this has a direct impact on our end users, since it makes our app feel faster and more responsive.

In this article, we would be looking at how to manage React Native state using React Query. Let’s get started with the prerequisites in the next section.

Prerequisites

  • Basic knowledge of JavaScript
  • Basic knowledge of React and React Native

Getting started with React Query and React Native

First, we create a new React Native application using Expo CLI as seen below:

npm i --global expo-cli

// bootstrap a new project
expo init <-- your app name -->

You will be prompted with a list of templates:

Expo CLI Template List

Choose the first one and change into your app directory by running:

cd <-- your app name -->

Next, install dependencies:

// install dependencies
npm i @react-navigation/native @react-navigation/stack axios 
react-native-gesture-handler react-native-safe-area-context react-native-screens 
react-query

Start the development server:

npm start 

In the next section, we will delve into React Query by building a simple application using data from JSON Placeholder.

Complex state management with React Query

Set up styling

We will start by creating a constants folder with a color.js file inside it. Add the following code to the color.js file:

export default {
    primary: '#202c41',
    border: '#c6c6c6',
    white: '#fff',
    gray: '#9ca5ab'
};

This is used to style our app.

Set up navigation

Next, we set up our navigation. Create a navigation folder with a Main.js file inside it.
Add the following code to the Main.js file:

import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import { Posts } from '../screens/Posts';
import { Post } from '../screens/Post';
const MainStack = createStackNavigator();
export const Main = () => (
    <MainStack.Navigator>
        <MainStack.Screen name="Home" component={Posts} />
        <MainStack.Screen name="Post" component={Post} />
    </MainStack.Navigator>
);

In the code above, we have set up navigation for two screens, namely Home and Post.
We will create the Posts and Post components in a moment.

Add React Query

Now we have to set up React Query. Modify our App.js file as seen below:

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 (
        <React.Fragment>
            <StatusBar style="auto" />
            <NavigationContainer>
                <QueryClientProvider client={queryClient}>
                    <Main />
                </QueryClientProvider>
            </NavigationContainer>
        </React.Fragment>
    );
}

In the code above, React Query uses the QueryClient to interact with a cache. And the QueryClientProvider component is used to connect and provide the QueryClient to our app.

Create the app’s screens

Following this, let’s create reusable components for our screens. Create a components folder with a Text.js file inside it. Add the following code to the Text.js:

import React from 'react';
import { StyleSheet, Text as RNText } from 'react-native';
import colors from '../constants/colors';
const styles = StyleSheet.create({
    defaultText: {
        color: colors.primary,
        fontSize: 18
    }
});
export const Text = ({ children, style = {} }) => {
    const textStyles = [ styles.defaultText ];
    textStyles.push(style);
    return <RNText style={textStyles}>{children}</RNText>;
};

Above, we have a reusable Text component with default style. We will use this across our app to style our texts.


More great articles from LogRocket:


Before we can create the screens — the Post.js and Posts.js files — we need to set up our hooks for fetching data rendered by these screens.

The Posts.js file renders all the posts gotten from JSON Placeholder, and it serves as the home screen while the Post.js file handles the details of each post.

Create two folders, namely screens and hooks. In the hooks folder, create a usePosts.js file and add the following code:

import { useQuery } from 'react-query';
import axios from 'axios';

const fetchPosts = async () => {
    const { data } = await axios.get('https://jsonplaceholder.typicode.com/posts');
    return data;
};

const usePosts = () => useQuery('posts', fetchPosts);
export default usePosts;

Also, create a usePost.js file and the following code:

import { useQuery } from 'react-query';
import axios from 'axios';

const fetchPost = async (postId) => {
    const { data } = await axios.get(`https://jsonplaceholder.typicode.com/comments?postId=${postId}`);
    return data;
};

const usePost = (postId) => useQuery([ 'posts', postId ], () => fetchPost(postId));
export default usePost;

In the code examples above, useQuery takes a unique identifier named posts as its first argument and a fetcher function as its second argument. We can now import both usePosts and usePost for use in our screens.

In the screens folder, create two files, namely Posts.js and Post.js. Add the following code to the Posts.js file:

import React from 'react';
import { View, StyleSheet, FlatList, TouchableOpacity } from 'react-native';
import usePosts from '../hooks/usePosts';
import { Text } from '../components/Text';
import colors from '../constants/colors';

export const Posts = ({ navigation }) => {
    const { data, isLoading, isSuccess } = usePosts();
    return (
        <View style={styles.container}>
            {isLoading && (
                <React.Fragment>
                    <Text>Loading...</Text>
                </React.Fragment>
            )}

            {isSuccess && (
                <React.Fragment>
                    <Text style={styles.header}>all posts</Text>
                    <FlatList
                        data={data}
                        style={styles.wrapper}
                        keyExtractor={(item) => `${item.id}`}
                        renderItem={({ item }) => (
                            <TouchableOpacity
                                onPress={() => 
                                  navigation.push('Post', { post: item })}
                                style={styles.post}
                            >
                                <View style={styles.item}>
                                    <Text style={styles.postTitle}>
                                      {item.title}
                                    </Text>
                                </View>
                            </TouchableOpacity>
                        )}
                    />
                </React.Fragment>
            )}
        </View>
    );
};

const styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: colors.white,
        padding: 10
    },
    wrapper: {
        flex: 1,
        paddingVertical: 30
    },
    item: {
        paddingVertical: 10,
        paddingHorizontal: 20
    },
    header: {
        textAlign: 'center',
        textTransform: 'capitalize',
        fontWeight: 'bold',
        fontSize: 30,
        color: colors.primary,
        paddingVertical: 10
    },
    post: {
        backgroundColor: colors.primary,
        padding: 15,
        borderRadius: 10,
        marginBottom: 20
    },
    postTitle: { color: colors.white, textTransform: 'capitalize' }
});

In the above code, we render a loading… text if isLoading is true. And we rendered our post list using FlatList if isSuccess is true.

Now we have:

RN FlatList for React Query project

Next, in the Post.js file, add the following code:

import React from 'react';
import { View, StyleSheet, ScrollView } from 'react-native';
import usePost from '../hooks/usePost';
import { Text } from '../components/Text';
import colors from '../constants/colors';

export const Post = ({ route }) => {
    const { post } = route && route.params;
    const { data: comments, isSuccess, isLoading } = usePost(post.id);
    return (
        <ScrollView style={styles.container}>
            <Text style={styles.header}>{post.title}</Text>
            <View style={styles.post}>
                <Text>{post.body}</Text>
            </View>
            {isLoading && <Text style={{ textAlign: 'center' }}>Loading...</Text>}

            {isSuccess && (
                <React.Fragment>
                    <Text style={styles.commentHeader}>Comments</Text>
                    {comments.map((comment) => (
                        <View key={comment.id} style={styles.post}>
                            <Text>{comment.body}</Text>
                            <Text>{comment.email}</Text>
                        </View>
                    ))}
                </React.Fragment>
            )}
        </ScrollView>
    );
};
const styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: colors.white,
        padding: 30
    },
    header: {
        textAlign: 'center',
        textTransform: 'capitalize',
        fontWeight: 'bold',
        fontSize: 40,
        color: colors.primary,
        paddingVertical: 10
    },
    commentHeader: {
        textAlign: 'center',
        textTransform: 'capitalize',
        fontWeight: 'bold',
        fontSize: 30,
        color: colors.primary,
        paddingVertical: 30
    },
    post: {
        flex: 1,
        paddingVertical: 10,
        alignItems: 'center'
    }
});

In the code above, we received the post clicked on by the user as a route param and fetched its comment using its ID. Below is an example of a post screen:

React Query Post Screen

Also, on first load time, you would see the loading for the post and the comment, but after this when you navigate back and forth, you won’t see them again. This is because React Query caches our data under the hood, making subsequent load time “instant.”

Conclusion

React Query is a well-thought-out data-fetching library that comes with a lot of interesting features. As we have seen, it offers a gain in DX and UX and also boosts our app performance.

I do hope that after going through this article you have learned enough to give it a try in your next project.

Lastly, you can get the full app on GitHub.

LogRocket: Instantly recreate issues in your React Native apps.

LogRocket is a React Native monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your React Native apps.

LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.

Start proactively monitoring your React Native apps — .

Lawrence Eagles Senior full-stack developer, writer, and instructor.

Leave a Reply