Learning how to create custom hooks with API resource

Learning how to create custom hooks with API resource

Introduction

Before now; it was pratically impossible to have a state in a functional component then comes React Hooks.

What is React Hooks?

I can not really give a specific definition to react hooks but what I can say is that, react-hooks provides functional API which makes it possible for us to use state(s) in a functional component

In a lay-man language: React hooks makes it possible to define states in a functional component using it's APIs.

We have different React Hooks (I will be covering this in another tutorial), but for now what i hope to achieve in this is to explain how to create a custom react-hooks;

A custom react hooks is a functional component that is built with the fundamental react hooks api (useState, useEffect etc); one of the ways to know a custom react hooks is by its name, which start with use eg useAPi

In this tutorial we'll be extending the api resource file by creating a custom react hooks which will help us handle the various 'states'/'life-cycle' while calling an API

basically for all APIs - the various things which are important to track are

  1. data - the result returned after calling the endpoint

  2. error - in a situation where there's a client or server error, we should be able to track the error returned

  3. loading - the current state i.e. if it still processing or not

  4. status code - the response status code from the server

Example of a custom-hook (useApi)

import { AxiosError } from 'axios';
import { useState } from 'react';
import { api } from 'utils'; // from https://akinolu52.hashnode.dev/creating-an-api-resource-file-using-axios

interface connectProps {
    method: 'get' | 'post' | 'patch' | 'delete' | 'put';
    values: object;
    url: string;
}

function useApi() {
    const [loading, setLoading] = useState<Boolean>(false);
    const [data, setData] = useState<Record<string, any> | null>(null);
    const [error, setError] = useState<AxiosError | null>(null);

    const connect = async ({ method, url, values }: connectProps) => {
        try {
            setLoading(true);
            let result = null;

            if (method === "get" || method === "delete") {
                result = await api[method](url);
            } else {
                result = await api[method]([url, values]);
            }

            setData(result);
        } catch (error) {
            setError(error);
        } finally {
            setLoading(false);
        }

        return { data, error };
    }

    return { connect, loading };
}

export default useApi;

useApi.tsx

Explanation

import { useState } from 'react';
import { api } from 'utils';

In the first line; I import useState which will be used to manage the local state of the custom hooks and then I import the api from utils which was previously implemented in this tutorial

interface connectProps {
    method: 'get' | 'post' | 'patch' | 'delete' | 'put';
    values: object;
    url: string;
}

This is the interface for the useApi-connect function (basically the types definitions of data to be passed to the function), here the connectProps included

  1. method: the request type
  2. values: the request data
  3. url: the request url
function useApi() {}

Notice that the function name start with use which signifies that we are in a custom hook

 const [loading, setLoading] = useState<Boolean>(false);
 const [data, setData] = useState<Record<string, any> | null>(null);
 const [error, setError] = useState<Record<string, any> | null>(null);

This are the custom hooks states, these states handles the loading, data and error information for the api being called.

  1. loading: this is a boolean value which is used to track the current state of the api
  2. data: this is used to hold the result of a successful API call eg response code 'OK'
  3. error: this is used to handle the error information of the api, this error state will not be empty if an error occur while processing the api call
const connect = async ({ method, url, values }: connectProps) => { ... }

This is the function that handles the API call, this function is called with the following parameters: method, url, values

  1. method: the request type
  2. values: the request data
  3. url: the request url
try {
    setLoading(true);
    let result = null;

    if (method === "get" || method === "delete") {
        result = await api[method](url);
    } else {
        result = await api[method]([url, values]);
    }

    setData(result);
}

In this block of code, the loading state is set to true, so as to signify that the api is in progress (will be processed)

for method get and delete, these api does not involve sending data to the backend so the api is only processed with the url

for the following methods: post, put and patch; certain data are sent to the to the backend server for processing hence the values are sent with the url in an array to the api function

the result of any of these request type is stored in the data state

catch (error) {
    setError(error);
}

This block handles an error, all it does is to store the error received as the response to the error state

finally {
    setLoading(false);
}

This block handles resetting of the loading state, i.e set the loading state to false which is the initial state.

How to use

import { yupResolver } from "@hookform/resolvers/yup";
import { Button, Input } from "components";
import { SubmitHandler, useForm } from "react-hook-form";
import { loginSchema } from "schemas";
import useApi from "useApi";

type FormData = {
  email: string;
  password: string;
};

function LoginForm() {
  const { connect, loading } = useApi();

  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<FormData>({
    resolver: yupResolver(loginSchema),
  });

  const onSubmit: SubmitHandler<FormData> = async (data) => {
    const { data, error } = await connect({
        method: "POST",
        url: 'https://some-fake-api.com/login',
        values: data
    });

    console.log('response from api: ', data, error);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div className="mb-6">
        <Input
          type="email"
          label="Email Address"
          {...register("email")}
          error={errors?.email}
        />
      </div>

      <div className="mb-6">
        <Input
          type="password"
          label="Password"
          {...register("password")}
          error={errors?.password}
        />
      </div>

      <div className="mb-3">
        <Button text="Login" type="submit" isLoading={loading} />
      </div>
    </form>
  );
}

export default LoginForm;

LoginForm.tsx

Footnotes

Thanks, for checking out this tutorial. (please like and add your comments) You can also check out my other tutorials on my blog

If you have any questions, feedback or comments, please let me know.

You can connect with me on twitter email github

You can also reach out to me(I do React-Native & React Js) twitter email