Complete Guidance to React Native Redux Thunk Axios Rest API Call 2021

In this tutorial we are going to learn how to use Rest API for for our redux CRUD project using redux thunk and axios.

Redux thunk axios

In previous tutorials we learned Basics of React Navigation, Redux setup and Redux Crud. In this tutorial we are going to learn how to add Rest API to our React native redux CRUD App using express js as backend.

We need to clone two repositories one is React native redux CRUD and another one is express js backend. You can learn express js from this express js tutorial.

let’s clone both repositories, make sure you clone both in a separate folders,

git clone https://github.com/mahisat/react-native-redux-example-crud.git
git clone https://github.com/mahisat/nodejs-expressjs-sequelize-mysql

After cloning go to react-native-redux-example-crud folder, we are going to make changes first in in our frontend react native.

npm install

Now we need to install 2 important libraries Redux Thunk and axios. Redux Thunk is a middleware used to perform async operations in redux. Axios is a Promise-based HTTP client for JavaScript, used to call api endpoints easily.

npm i redux-thunk axios

Redux Thunk Configuration

Let’s add redux thunk middleware to the redux store.

Go to App.js and import ReduxThunk from ‘redux-thunk’ and then change the store variable like this const store = createStore(rootReducer, {}, applyMiddleware(ReduxThunk));

Here is the modified App.js

import React from "react";
import { SafeAreaView, Text } from "react-native";
import { Provider } from "react-redux";
import { applyMiddleware, createStore } from "redux";
import { rootReducer } from "./src/reducers";
import MainNavigation from "./src/navigations/MainNavigation";
import ReduxThunk from "redux-thunk";

const store = createStore(rootReducer, {}, applyMiddleware(ReduxThunk));

const App = () => {
  return (
    <Provider store={store}>
      <SafeAreaView style={{ flex: 1 }}>
        <MainNavigation />
      </SafeAreaView>
    </Provider>
  );
};

export default App;

We are going to learn redux Axios CRUD operations. Now Go to QuoteAction.js file modify the code with below code.

Redux Thunk axios CRUD Operations

import {
  SAMPLE_ACTION,
  ADD_QUOTE,
  UPDATE_QUOTE,
  DELETE_QUOTE,
  LIST_QUOTES,
  LOADING,
} from "./types";
import axios from "axios";
import { MyToast } from "../components";

export const sampleQuoteAction = () => ({
  type: SAMPLE_ACTION,
});

const isLoading = (name, value) => {
  return async (dispatch, getState) => {
    dispatch({
      type: LOADING,
      data: {
        name: name,
        value: value,
      },
    });
  };
};

//List Quote
const baseUrl = "http://localhost:3000";
export const listQuotes = () => {
  return async (dispatch, getState) => {
    dispatch(isLoading("isListQuoteLoading", true));
    try {
      const response = await axios.get(`${baseUrl}/api/listQuotes`);
      console.log("respojse", response);
      if (response.status === 200) {
        dispatch(isLoading("isListQuoteLoading", false));

        return dispatch({
          type: LIST_QUOTES,
          data: response.data.quotes,
        });
      }
    } catch (error) {
      dispatch(isLoading("isListQuoteLoading", false));
      // console.log('error', error);
      if (error.response) {
        MyToast(error.response.data.message);
      } else if (error.request) {
        // console.log('error.request', error.request);
        // The request was made but no response was received
        // Error details are stored in error.reqeust
        if (error.request._response) {
          MyToast(error.request._response);
        }
      } else {
        // Some other errors
        MyToast(error.message);
      }
    }
  };
};

export const addQuote = ({ quote, author, navigation }) => {
  return async (dispatch, getState) => {
    const header = {
      headers: {
        "Content-Type": "application/json",
      },
    };
    let data = {
      quote: quote,
      author: author,
    };
    dispatch(isLoading("isSubmitQuoteLoading", true));

    try {
      const response = await axios.post(
        `${baseUrl}/api/createQuote`,
        data,
        header
      );
      if (response.status === 200) {
        dispatch(isLoading("isSubmitQuoteLoading", false));
        MyToast(response.data.message);
        dispatch({ type: ADD_QUOTE, data: response.data.quote });
        navigation.goBack();
      }
    } catch (error) {
      dispatch(isLoading("isSubmitQuoteLoading", false));

      // console.log('error', error);
      if (error.response) {
        MyToast(error.response.data.message);
      } else if (error.request) {
        if (error.request._response) {
          MyToast(error.request._response);
        }
      } else {
        MyToast(error.message);
      }
    }
  };
};

export const updateQuote = ({ author, quote, id, navigation }) => {
  return async (dispatch, getState) => {
    const header = {
      headers: {
        "Content-Type": "application/json",
      },
    };
    let data = {
      author: author,
      quote: quote,
    };
    dispatch(isLoading("isSubmitQuoteLoading", true));

    try {
      const response = await axios.put(
        `${baseUrl}/api/updateQuote/${id}`,
        data,
        header
      );
      if (response.status === 200) {
        dispatch(isLoading("isSubmitQuoteLoading", false));
        MyToast(response.data.message);
        dispatch({
          type: UPDATE_QUOTE,
          data: { quote: quote, author: author, id: id },
        });
        navigation.goBack();
      }
    } catch (error) {
      dispatch(isLoading("isSubmitQuoteLoading", false));

      // console.log('error', error);
      if (error.response) {
        MyToast(error.response.data.message);
      } else if (error.request) {
        if (error.request._response) {
          MyToast(error.request._response);
        }
      } else {
        MyToast(error.message);
      }
    }
  };
};

export const deleteQuote = ({ id }) => {
  return async (dispatch, getState) => {
    const header = {
      headers: {
        "Content-Type": "application/json",
      },
    };
    try {
      const response = await axios.put(
        `${baseUrl}/api/deleteQuote/${id}`,
        {},
        header
      );
      if (response.status === 200) {
        MyToast(response.data.message);

        dispatch({
          type: DELETE_QUOTE,
          data: {
            id,
          },
        });
      }
    } catch (error) {
      // console.log('error', error);
      if (error.response) {
        MyToast(error.response.data.message);
      } else if (error.request) {
        if (error.request._response) {
          MyToast(error.request._response);
        }
      } else {
        MyToast(error.message);
      }
    }
  };
};

We can see the explanation of listQuotes function,

isLoading function is used to display activity indicator when the API call is made. When an API calls begins we set it to true and we must set it to false when the request is successful as well as failure. We are using separate loading variables(isListQuoteLoading, isSubmitQuoteLoading) for the list and form submit.

Then we are making a request to listQuotes using axios. If response status is 200 we are making isListQuoteLoading to false and store the received quote response from the API to our redux store.

In catch block there is If else condition on error. It’s because when we receive any validation message from the server. we can’t display it using error.message. we can display it only through error.response. For other generic error from server we are using error.message to display it in toast. Read more about 400 bad request here in stack overflow

I hope you can understand create, update and delete functions. If you don’t understand please put it in comment section.

MyToast.js

We are creating our custom component to display message in toast. Go to Components folder create MyToast.js file

import { ToastAndroid, Platform, View } from "react-native";
import { Toast } from "native-base";
export default MyToast = (text) =>
  Platform.OS === "android"
    ? ToastAndroid.showWithGravityAndOffset(
        text,
        ToastAndroid.LONG,
        ToastAndroid.BOTTOM,
        25,
        50
      )
    : Toast.show({
        text: text,
        position: "bottom",
        duration: 3000,
      });

Index.js file Inside Components and Screens folder

Next, we are creating an index.js file inside the components folder.

import MyHeader from './MyHeader';
import MyToast from './MyToast';
export {MyHeader, MyToast};

Now we can import components in any page using single line command import {MyHeader, MyToast} from ‘../components’;

Similar to component we are creating index.js file inside Screens folder,

import HomeScreen from "../screens/HomeScreen";
import ListQuotes from "../screens/ListQuotes";
import QuoteForm from "../screens/QuoteForm";
import SettingsScreen from "../screens/SettingsScreen";

export { HomeScreen, ListQuotes, QuoteForm, SettingsScreen };

Inside MainNavigation.js import screens with this line import {HomeScreen, SettingsScreen, ListQuotes, QuoteForm} from ‘../screens’;

QuoteReducer.js

Modify the existing quotereducer.js with below code

import {
  SAMPLE_ACTION,
  ADD_QUOTE,
  UPDATE_QUOTE,
  DELETE_QUOTE,
  LIST_QUOTES,
  LOADING,
} from "../actions/types";

const INITIAL_STATE = {
  quotes: [],
  isListQuoteLoading: false,
  isSubmitQuoteLoading: false,
};
export default (state = INITIAL_STATE, action) => {
  switch (action.type) {
    case SAMPLE_ACTION:
      return state;
    case LOADING:
      return { ...state, [action.data.name]: action.data.value };

    case LIST_QUOTES:
      return { ...state, quotes: action.data };
    case ADD_QUOTE:
      let { quote } = action.data;

      const quotes = [quote, ...state.quotes]; //add the new quote to the top

      return { ...state, quotes: quotes };
    case UPDATE_QUOTE: {
      let { quote } = action.data;

      //check if quote already exist
      const quoteIndex = state.quotes.findIndex((obj) => obj.id === quote.id);

      return {
        ...state,
        quotes: [
          ...state.quotes.slice(0, quoteIndex),
          quote,
          ...state.quotes.slice(quoteIndex + 1),
        ],
      };
    }
    case DELETE_QUOTE: {
      let { id } = action.data;

      const quoteIndex = state.quotes.findIndex((obj) => obj.id === id);

      const quotes = [
        ...state.quotes.slice(0, quoteIndex),
        ...state.quotes.slice(quoteIndex + 1),
      ];

      return { ...state, quotes: quotes };
    }

    default:
      return state;
  }
};

Types.js

We added two new types in this project ListQuote and Loading,

export const SAMPLE_ACTION = "SAMPLE_ACTION";
export const ADD_QUOTE = "ADD_QUOTE";
export const UPDATE_QUOTE = "UPDATE_QUOTE";
export const DELETE_QUOTE = "DELETE_QUOTE";
export const LIST_QUOTES = "LIST_QUOTES";
export const LOADING = "LOADING";

ListQuotes.js

In the listQuote.js, we are importing the listQuote action we created earlier. Then using useEffect method we are dispatching it to make a api call.

import {deleteQuote, listQuotes} from ‘../actions’;

useEffect(() => { dispatch(listQuotes()); }, []);

Modified ListQuote.js

import React, { useEffect } from "react";
import {
  Text,
  View,
  FlatList,
  StyleSheet,
  ActivityIndicator,
} from "react-native";
import { Button, Fab, Icon } from "native-base";
import { useNavigation } from "@react-navigation/native";
import { useDispatch, useSelector } from "react-redux";

import { deleteQuote, listQuotes } from "../actions";
import { MyHeader } from "../components";

export const ListQuotes = () => {
  const navigation = useNavigation();
  const { quotes, isListQuoteLoading } = useSelector(
    (state) => state.QuoteReducer
  );
  const dispatch = useDispatch();
  useEffect(() => {
    dispatch(listQuotes());
  }, []);
  const renderItem = ({ item, index }) => {
    return (
      <View
        style={{
          margin: 10,
          padding: 10,
          backgroundColor: index % 2 == 0 ? "red" : "violet",
          elevation: 3,
          borderRadius: 10,
        }}
      >
        <Text style={{ color: "#fff" }}>{item.quote}</Text>
        <Text style={{ color: "#fff", textAlign: "right" }}>
          - {item.author}
        </Text>

        <View style={{ flexDirection: "row", justifyContent: "space-around" }}>
          <Button
            style={styles.buttonCointainer}
            block
            onPress={() =>
              navigation.navigate("QuoteForm", { quoteParams: item })
            }
          >
            <Text style={styles.buttonText}> Update </Text>
          </Button>
          <Button
            style={styles.buttonCointainer}
            block
            onPress={() => dispatch(deleteQuote({ id: item.id }))}
          >
            <Text style={styles.buttonText}> Delete </Text>
          </Button>
        </View>
      </View>
    );
  };
  return (
    <View style={styles.container}>
      <MyHeader title="List Quotes" />
      {isListQuoteLoading && <ActivityIndicator size="large" color="blue" />}

      {quotes && quotes.length > 0 ? (
        <FlatList
          data={quotes}
          renderItem={renderItem}
          keyExtractor={(item, index) => index.toString()}
        />
      ) : (
        <View
          style={{ justifyContent: "center", alignItems: "center", flex: 1 }}
        >
          <Text style={{ fontSize: 20 }}>No data found</Text>
        </View>
      )}
      <Fab
        direction="up"
        style={{ backgroundColor: "red" }}
        position="bottomRight"
        onPress={() =>
          navigation.navigate("QuoteForm", { quoteParams: undefined })
        }
      >
        <Icon name="add" type="MaterialIcons" />
      </Fab>
    </View>
  );
};

export default ListQuotes;

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  buttonCointainer: {
    backgroundColor: "#fff",
    margin: 10,
  },
  buttonText: {
    color: "#000",
  },
});

QuoteForm.js

In this page we passed navigation params to add and update action creators. Next we added activity indicator to display when the save button is pressed.

Here is the modified QuoteForm.js

import React, { useState } from "react";
import {
  View,
  Text,
  StyleSheet,
  ScrollView,
  ActivityIndicator,
} from "react-native";
import { useNavigation } from "@react-navigation/native";
import { Form, Item, Input, Label, Textarea, Button } from "native-base";
import { addQuote, updateQuote } from "../actions";
import { useDispatch } from "react-redux";
import { MyHeader } from "../components";

export const QuoteForm = (props) => {
  const navigation = useNavigation();
  const { quoteParams } = props.route.params;

  const [author, setAuthor] = useState(quoteParams ? quoteParams.author : "");
  const [quote, setQuote] = useState(quoteParams ? quoteParams.quote : "");
  const { isSubmitQuoteLoading } = useSelector((state) => state.QuoteReducer);
  const dispatch = useDispatch();
  const onSubmit = () => {
    let edit = quoteParams !== undefined;
    edit
      ? dispatch(
          updateQuote({
            quote: quote,
            author: author,
            id: quoteParams.id,
            navigation: navigation,
          })
        )
      : dispatch(
          addQuote({
            quote: quote,
            author: author,
            navigation: navigation,
          })
        );
  };
  return (
    <View style={styles.container}>
      <MyHeader title={"Add Quote"} back={() => navigation.goBack()} />
      <ScrollView style={{ margin: 20 }}>
        <Item stackedLabel>
          <Label>Author</Label>
          <Input value={author} onChangeText={(text) => setAuthor(text)} />
        </Item>
        <Textarea
          rowSpan={5}
          bordered
          placeholder="Quote"
          onChangeText={(text) => setQuote(text)}
          value={quote}
        />
        {isSubmitQuoteLoading ? (
          <ActivityIndicator size="large" color="blue" />
        ) : (
          <Button style={styles.buttonCointainer} block onPress={onSubmit}>
            <Text style={styles.buttonText}> Save </Text>
          </Button>
        )}
      </ScrollView>
    </View>
  );
};
export default QuoteForm;

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  buttonCointainer: {
    backgroundColor: "#ff6347",
    margin: 10,
  },
  buttonText: {
    color: "#fff",
  },
});

In react native we connected redux axios to call api. Now we need to start our express js server to test the app.

Go to nodejs-expressjs-sequelize-mysql folder we cloned earlier. Run npm install command to install dependencies. Make sure you started your localhost server to connect to databse. Now run the command nodemon to start our express js server. You can read how to create express js api here

When you are testing the react native app on the real mobile device localhost express js server couldn’t connect. There is a solution for this we need to install ngrok software. Ngrok will give us remote address to access our localhost express js api.

After installing the ngrok, open it and run a command ngrok http 3000. Here 3000 is our express js api port number.

ngrok http 3000

Ngrok

Now to your react native app and then go to QuoteActions.js file change the base url with ngrok provided url https://17472b53aea9.ngrok.io (this should be different for you). Once you changed the URL. you can run the react native app.

react-native run-android

conclusion:

We successfully learned how to use redux thunk and Axios in react native application. If you have any doubt or encounter any problem during this please put it in a comment section. I can clarify your doubts.

You can get source code for this react native redux thunk axios project from github

Here is the next part React Native NetInfo to Detect Internet Connection and display it to users.