React helpers and patterns in 2019

Part 1: The useApi Hook for data fetching

Have you seen custom hooks for fetching data? If you haven't, don't worry (you could read this). I just wanted to share mine, which is a little different. In contrary to other useFetch methods, mine allows you to pass your own API function as an argument. This gives you more freedom and better separation of concerns. First, let's enumerate what this custom hook does for you.

The hook...

  1. Manages the response state (data, loading, error and success).
  2. Calls your own fetch function (axios for example), which you pass in as an argument.
  3. Can be used multiple times in the same React Component.
  4. Is independent of which HTTP method you use in the fetch function .

And optionally:

  1. Set an initial value for data. Especially Useful when using TypeScript.
  2. TypeScript.
  3. A small performance optimization.

This however, won't be covered in detail in this article.

The useApi hook

export function useApi(apiFunction) {
  const [response, setResponse] = React.useState({
    data: null,
    hasErrors: false,
    isFetching: false,
    isSuccess: false,
  });

  const fetchMethod = async () => {
    setResponse((prevState) => ({
      ...prevState,
      loading: true,
    }));
    try {
      const apiData = await apiFunction();
      setResponse({
        data: apiData,
        isFetching: false,
        hasErrors: false,
        isSuccess: true,
      });
    } catch (error) {
      setResponse({
        data: null,
        isFetching: false,
        hasErrors: true,
        isSuccess: false,
      });
    }
  };

  return [response, fetchMethod];
}

The big 'gotchas' in this hook are the fetchMethod and the apiFunction argument. You use the hook by calling fetchMethod, which executes the apiFunction you pass in as an argument.

Example: Fetching users with the useApi hook

Using the hook is very straightforward. Notice how you don't have to use the same names as in the useApi hook.

import { fetchUsers } from '~/api/users';
import { useApi } from '~/hooks/useApi';

const UsersPage = () => {
  const [usersResponse, getUsers] = useApi(fetchUsers);

  React.useEffect(() => {
    getUsers();
  }, [getUsers]);

  // Do what you like with these destructured properties.
  const { data, hasErrors, isFetching, isSuccess } = usersResponse;

  if (hasErrors) {
    return null;
  }

  return <>{data.map((user) => user.name)}</>;
};

What are we doing here?

  1. We name the response object usersResponse, and the 'fetchMethod' part getUsers
  2. We pass the fetchUsers api function which is the ACTUAL HTTP function (more on this soon). Just remember this could be your axios function which has the HTTP method, API endpoint etc.
  3. We use the React.useEffect hook to fetch this data when the component/page mounts.
  4. Finally, we render the users.

You might be thinking: Ok cool ZakKa, but what makes this hook stand out? Let's take a break from this hook itself so I can explain some of my reasoning behind this approach.

Separation of concerns

Remember when I said:

In contrary to other useFetch methods, mine allows you to pass your own API function as an argument

Most of the time I do a lot of data validation and transforms before I return the data - or throw the errors 🀭 - from the HTTP request. I also don't want to mixup the reusable function for apiCalls with the hook. To demonstrate: I usually create a reusable function based on this, in combination with different API functions for each type of HTTP request:

/api/users.js

import { getClientReadyUser, validateUser } from '../models/user';
import { axiosRequest } from '.'; // <- reusable axios function

async function fetchUser(userId) {
  try {
    const user = await axiosRequest('get', `/users/v1/${userId}`);
    const safeUser = validateUser(user); // data validation
    const clientReadyUser = getClientReadyUser(user); // data transforms

    return clientReadyProduct;
  } catch (err) {
    throw err;
  }
}

I want the code that's validating and transforming the data separate from the hook. We don't need all of that to be inside the 'React' side of the codebase. Same goes for AXIOS/ 'vanilla' fetch / or any other HTTP client. Also, same goes for the HTTP method (get/post/patch etc). I want to keep the hook simple! All of that needs to be handled before the data is received inside my precious useApi hook.

How I do all of that HTTP/data stuff is beyond the scope of this article. First, let's get back to the hook!

Posting a user with the useApi hook

So we've covered fetching, but what about posting? Not much difference, we'll add the code to the previous example:

import { fetchUsers, postUser } from "~/api/users"
import { useApi } from "~/hooks/useApi"

const UsersPage = () => {
  const [usersResponse, getUsers] = useApi(fetchUsers);
  const [createUserResponse, createUser] = useApi(() =>
    postUser({
      firstName: 'Musa',
      lastName: 'ibn Zakaria',
    }),
  );

  React.useEffect(() => {
    getUsers();
  }, [getUsers]);

  // Do what you like with these destructured properties.
  const { data: users, hasErrors, isFetching, isSuccess } = usersResponse;

  if (hasErrors || isFetching) {
    return null;
  };

  return (
    <>
      <ul>
        {users.map(user => <li key={user.id}>{user.name}<li>)}
      </ul>
      <button onClick={createUser}>Submit</button>
    </>
  );
};

As you can see, we can use the useApi hook multiple times without issue. If you are confused by data: users : This is optional and simply a cool way to rename destructured properties. It can be useful when you need data from multiple sources.

That's it! You don't need much more than this. I will cover other aspects like error handling, the actual API fetch functions and more to give you a more complete picture.

If you want to see a more advanced implementation, checkout the next chapter:

Optional: TypeScript, useCallBack and a custom initial value

The code I use myself has a bit more to it:

Check it out below and feel free to contact me if you have any questions about the code on my gist

The Hook with TypeScript and initialDataValue

import { ErrorType } from '~/core/api';

type ReturnType<DataType> = [
  {
    data: DataType;
    loading: boolean;
    isSuccess: boolean;
    errorMessage: ErrorType['message'];
    errorData: ErrorType['data'];
  },
  () => {} // <-- wip
];

export function useApi<T>(apiFunction, initialDataValue): ReturnType<T> {
  const [response, setResponse] = React.useState({
    data: initialDataValue,
    errorMessage: null,
    errorData: null,
    isFetching: false,
    isSuccess: false,
  });

  const fetchData = React.useCallback(async () => {
    setResponse((prevState) => ({
      ...prevState,
      loading: true,
    }));
    try {
      const apiData = await apiFunction();
      setResponse({
        data: apiData,
        isFetching: false,
        errorMessage: null,
        errorData: null,
        isSuccess: true,
      });
    } catch (error) {
      setResponse({
        data: null,
        isFetching: false,
        errorMessage: error.message,
        errorData: error.data,
        isSuccess: false,
      });
    }
  }, [apiFunction]);

  return [response, fetchData];
}

Usage inside your component

type UsersType = {
  firstName: string;
  lastName: string;
};
const [usersResponse, getUsers] = useApi<UsersType[]>(fetchUsers, []);

Part 2: Ready to Render

Before we can safely render received data from an API, there is a lot to take into account:

  1. Did the request finish? Or are we still waiting on the API?
  2. Was the request itself a success or do we have errors?
  3. If there was an error, what type? In the response, or in setting up the request?
  4. Does the response have the expected data?
  5. Was this expected data safely sanitized/transformed (for example snake_case to camelCase or fallbacks per field)?

I will only cover parts of 1 and 2 in this article. The scope will stay within the React component. Error handling, HTTP requests and data transformation + validation will be covered in another article.

Y'all ready for this?

Before rendering my data...

For my React component, I can keep it a bit simpler than above. In a component, I want to know 3 things:

  1. Did the request finish? Or are we still waiting on the API?
  2. Are there any errors? If so, which?
  3. After the request completes, do I even have data?

The reason why this is more straightforward is because the more detailed points above should've already been dealt with. Where? In your HTTP requests, data validation + transforms and the code for your error object. (See the 'Separation of concerns' part in my The Custom useApi Hook article)

No data?

The third point might not be clear, so let me clarify. This might trigger you though! Ever did some troubleshooting with console.log(apiData) and saw this [] or this {} staring you in the face?

Empty objects and arrays are no fun, and honestly, you could (and probably should) prevent this from happening on a higher level in your code architecture: The part in your code where you do data transformations from the API. But you probably have worked with empty objects and arrays because of other mistakes, like:

So that's why we're also going to check for empty data 😁.

On the other hand. An empty object or array isn't always wrong. In a lot of cases, it's a valid state. All I'm trying to say is, do something with it. A valid state for your use case or not: Be ready

Let's get ready to render!

I could check the 3 points explained above with the following helper function:

DataIsReady helper function

export const dataIsReady = ({ isFetching, hasErrors, data }) =>
  !isFetching && !hasErrors && !isEmpty(data);

As you can see this function only returns true or false.

The isEmpty function is from You-Dont-Need-Lodash-Underscore.

We can use the function in our UsersPage like this:

const UsersPage = () => {
  const [usersResponse, getUsers] = useApi(fetchUsers);

  React.useEffect(() => {
    getUsers();
  }, [getUsers]);

  // dataIsReady expects { isFetching, hasErrors, data }
  const readyToRender = dataIsReady({ ...usersResponse });

  if (!readyToRender) {
      return null
  };

  return (
    <>
      <ul>
        {users.map(user => <li key={user.id}>{user.name}<li>)}
      </ul>
      <button onClick={createUser}>Submit</button>
    </>
  );

I'm passing everything from the environmentResponse object to dataIsReady since the keys I need in the object have the exact same name (isFetching, hasErrors and data). Just like in my previous article, I am not demonstrating the error handling but just have the hasErrors flag here.

You can also check the useApi hook in Modern React App Part 1: The Custom useApi Hook for more information about this custom hook.

Note: Whenever I write readyToRender, I always imagine Michael/Bruce Buffer shouting 'Let's get ready to renderrrrrrrrrr!' like in the following video: Michael Buffer - Let's Get Ready To Rumble!!!

Further customizing

I've just demonstrated the principle of checking if we can render data. However, we might also want:

Loading indicator example:

if (usersResponse.isFetching) {
  return <>Loading...</>;
}

Another thing to think about is what data we actually need before we can render a component or part of a component. In a lot of cases, we only want to prevent the rendering of a certain elements.

For example:

return (
  <>
    {usersResponse.isFetching ? "Fetching users..." : (
      <ul>
        {users.map(user => <li key={user.id}>{user.name}<li>)}
      </ul>
    )}
    <button onClick={createUser}>Submit</button>
  </>
);

In above example, the button can still be rendered even if we are still waiting for the response.

It's better to move the logic inside a separate function/reusable component. Even more so because we want to code for more specific use cases, like rendering the errors and prevent the rendering of <ul> when there is no data at all. You could do something like !users.length ? "No users found" : //render users but this can cause your JSX it be filled with too much logic. Rather, let a function or component handle that. I'll show you how in the next part of this series: The Render Status Component

Part 3: Render Status Component

Previously, I covered a simple principle for knowing when data is ready to render:

  1. Data is done fetching (done 'loading')
  2. Data has no errors
  3. There is data (data object/array isn't empty)

In this article, I just want to demonstrate how you can separate your React Component into two parts:

I will explain the how before the why this time.

Ok, How?

We simply have a separate component which renders errors or a loading indicator.

The component

const RenderStatus = ({ hasErrors, isLoading, children }) => {
  if (isLoading) {
    return <>Loading...</>; // place your own spinner/loading screen here
  }

  if (hasErrors) {
    return (
      // Customize this later to actually render the errors
      <>An Error has occurred!</>
    );
  }

  return <>{children}</>;
};

export default RenderStatus;

Usage in your page

// dataIsReady expects { isFetching, hasErrors, data }
const readyToRender = dataIsReady({ ...usersResponse });

if (!readyToRender) {
  return (
    <MainLayout>
      <RenderStatus {...usersResponse} />
    </MainLayout>
  );
}

You can read Modern React App Part 2: Ready to Render for the dataIsReady explanation.

Ok, why?

Example of rendering errors

This should be specific for the API you are working with, and the way your error object looks. I'll show you an example of mine, but you should be aware that you have to research on your own what's best in your use case.

In my case, my errorObject initializes like this:

const errorObject = {
  data: null,
  message: null,
};

And after an API call, it can look like this:

const errorObject = {
  data: {
    usersettings: [
      'Username must be longer than 3 characters',
      'Password must have at least one special character',
    ],
  },
  message: '409: Conflict',
};

Here, the response data is an object. But I've also had cases with this particular API in which I simply got a plain/text back:

const errorObject = {
  data: "You are not authorized"
  message: '403: Forbidden',
};

So I have to basically expect both objects and strings:

const RenderStatus = ({ errorData, errorMessage, loading }) => {
  if (loading) {
    return <>Loading...</>;
  }

  if (errorMessage) {
    const renderErrors = (errData) => {
      // check if errorResponse is object because API sometimes returns plain text.
      if (!isObject(errData)) {
        return errorData;
      }
      return Object.entries(errData).map(([errorKey, errorMessages]) => (
        <div key={errorKey}>
          <h3>{errorKey}</h3>
          {errorMessages && (
            <ul>
              {errorMessages.map((errMessage, i) => (
                <li key={i}>{errMessage}</li>
              ))}
            </ul>
          )}
          <br />
        </div>
      ));
    };
    return (
      <div>
        <h2>{errorMessage}</h2>
        {errorData && renderErrors(errorData)}
      </div>
    );
  }

  return null;
};

export default RenderStatus;

the isObject function:

export const isObject = (value): boolean => {
  return value && typeof value === 'object' && value.constructor === Object;
};

Again, this is just an example. You'll need to figure out the best way to build and display your own error data. Be sure what you can expect from the API, figure out the edge cases and how you want to render your error data.

Optional: Give it children

You might want to wrap the Render status component around your page or component.

const RenderStatus = ({ hasErrors, isLoading, children }) => {
  if (isLoading) {
    return <>Loading...</>; // place your own spinner/loading screen here
  }

  if (hasErrors) {
    return (
      // Customize this later to actually render the errors
      <>An Error has occurred!</>
    );
  }

  return <>{children}</>;
};

export default RenderStatus;