<repo_name>jmpolitzer/use-api-request<file_sep>/src/useApiRequest/index.js
import { useReducer, useCallback } from "react";

import { handleFetching, handleSuccess, handleError } from "./actions";
import buildReducer from "./reducer";

function useApiRequest({ axios, key, debug }) {
  const initialState = {
    fetching: [],
    resources: {},
    errors: {}
  };

  const reducer = useCallback(buildReducer(debug), [key]);
  const [state, dispatch] = useReducer(reducer, initialState);

  const replaceParams = (url, response) => {
    const matches = url.match(/\{{.*?\}}/g);

    if (matches && matches.length > 0) {
      const params = matches.map(match =>
        match.replace("{{", "").replace("}}", "")
      );

      return matches.reduce((acc, match, i) => {
        const param = params[i].split(".").reduce((pAcc, fragment, j) => {
          return pAcc[fragment];
        }, response);

        return acc.replace(match, param);
      }, url);
    } else {
      return url;
    }
  };

  const onError = (resource, error) => {
    dispatch(handleError(resource, error));
  };

  const makeApiRequest = (config, prevResponse) => {
    const resource = Object.keys(config)[0];
    const { next, isNext, url, ...rest } = config[resource];
    const request = { ...rest, url: replaceParams(url, prevResponse) };

    if (!isNext) dispatch(handleFetching([resource]));

    axios
      .request(request)
      .then(response => {
        if (next) {
          const nextConfig = { [resource]: { ...next, isNext: true } };

          makeApiRequest(nextConfig, response);
        } else {
          dispatch(handleSuccess([resource], { [resource]: response }));
        }
      })
      .catch(e => onError([resource], { [resource]: e }));
  };

  const makeApiRequests = config => {
    const resources = Object.keys(config);

    dispatch(handleFetching(resources));

    Promise.all(
      resources.map(key =>
        axios.request({
          ...config[key],
          headers: {
            resourceKey: key
          }
        })
      )
    )
      .then(responses => {
        const response = resources.reduce((acc, resource, i) => {
          acc[resource] = responses[i];

          return acc;
        }, {});

        dispatch(handleSuccess(resources, response));
      })
      .catch(e => {
        const resource = e.config.headers.resourceKey;

        return onError(resources, { [resource]: e });
      });
  };

  return { state, makeApiRequest, makeApiRequests };
}

export default useApiRequest;
<file_sep>/src/useApiRequest/reducer.js
import logger from './helpers/logger'

const filterFetching = (action, state) =>
  state.fetching.filter(
    resource => !action.payload.resource.includes(resource)
  )

const filterResourcesOrErrors = (resource, resources) => {
  const r = Object.keys(resources).reduce((acc, res) => {
    if (!resource.includes(res)) {
      acc[res] = resources[res]
    }

    return acc
  }, {})

  return r
}

function createReducer() {
  return function(state, action) {
    const { resource } = action.payload

    switch (action.type) {
      case `${resource}/FETCHING`:
        return {
          fetching: [...state.fetching, action.payload.resource].reduce(
            (acc, val) => acc.concat(val),
            []
          ),
          resources: filterResourcesOrErrors(
            action.payload.resource,
            state.resources
          ),
          errors: filterResourcesOrErrors(action.payload.resource, state.errors)
        }
      case `${resource}/SUCCESS`:
        return {
          fetching: filterFetching(action, state),
          resources: {
            ...state.resources,
            ...action.payload.response
          },
          errors: filterResourcesOrErrors(action.payload.resource, state.errors)
        }
      case `${resource}/ERROR`:
        return {
          fetching: filterFetching(action, state),
          resources: filterResourcesOrErrors(
            action.payload.resource,
            state.resources
          ),
          errors: {
            ...state.errors,
            ...action.payload.error
          }
        }
    }
  }
}

function buildReducer(debug) {
  const reducer = createReducer()

  /* istanbul ignore next */
  if (debug === true && process.env.NODE_ENV === 'development') {
    return logger(reducer)
  } else {
    return reducer
  }
}

export default buildReducer
<file_sep>/README.md
# use-api-request

> A simple React hook for making network requests using Axios.

[![NPM](https://img.shields.io/npm/v/use-api-request.svg)](https://www.npmjs.com/package/use-api-request)
[![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
[![Build Status](https://travis-ci.com/jmpolitzer/use-api-request.svg?branch=master)](https://travis-ci.com/jmpolitzer/use-api-request)
[![Coverage Status](https://coveralls.io/repos/github/jmpolitzer/use-api-request/badge.svg?branch=master)](https://coveralls.io/github/jmpolitzer/use-api-request?branch=master)

## Install 

```
npm install --save use-api-request
```

## Example

```javascript
import axios from 'axios';
import { useApiRequest } from 'use-api-request';

function MyComponent() {
  const apiConfig = {
    axios: axios.create({
        baseURL: "https://whereami.com/"
    }),
    key: "example",
    debug: true
  };

  const { state, makeApiRequest } = useApiRequest(apiConfig);
  const { fetching, resources: { posts }, errors } = state;

  const requestConfig = {
     posts: {
        url: "/posts"
      }
  };

  return (
    <>
      <button onClick={() => makeApiRequest(requestConfig)}>Get Posts</button>
      <>
        {fetching.includes("posts") && <div>fetching</div>}
        {posts.data.length > 0 && posts.data.map(post =>
            <div key={post.id}>{post.title}</div>) }
        {errors.posts && <div>{errors.posts.message}</div>}
      </>
    </>
  );
}
```

## Usage

1. Pass a config object with an Axios instance and a unique `key` string into `useApiRequest`. The Axios instance takes the exact same configurations as specified in the [documentation](https://www.npmjs.com/package/axios). Optionally include a `debug` flag if you'd like to see redux-like logging in your console.

2. Create a request object -- again, refer to the [Axios documentation](https://www.npmjs.com/package/axios) for examples -- and pass it into `makeApiRequest`. Note that this library uses `Axios.request()` under the hood. The request object is nothing more than an Axios request config object assigned to a resource key of your choosing. So, if you name the key `things`, you will find `things` many times within the state object. See below for more details.

3. Call `makeApiRequest` and look for your response on the resources key of the `state` object provided by `useApiRequest`.

## Features

Make a single request:
    
 ```javascript
 const singleRequestConfig = {
    posts: {
      url: "/posts"
    }
  };

 makeApiRequest(singleRequestConfig);
 ```
   
Make multiple concurrent requests:
  
  ```javascript
  const concurrentRequestsConfig = {
      albums: {
        url: "/albums"
      },
      users: {
        url: "/users"
      }
    };

  makeApiRequests(concurrentRequestsConfig);
  ```

Make a sequential request (dependent variables are wrapped in double curly brackets and will always be properties of `data`): 

  ```javascript
  const sequentialRequestConfig = {
      userPosts: {
        url: "/posts/1",
        next: {
          url: "/posts?userId={{data.userId}}"
        }
      }
    };

  makeApiRequest(sequentialRequestConfig);
  ```
  
## API

```javascript
const { state, makeApiRequest, makeApiRequests } = useApiRequest(apiConfig);
const { fetching, resources, errors } = state;
```

`fetching` - array of strings corresponding to the resource key(s) used in the request config

`resources` - object of successful responses; each response can be found under the key used in the request config, and each response contains the entire response generated by Axios

`errors` - object of errors; each error can be found under the key used in the request config, and each error contains the entire error generated by Axios
 
## License

MIT © [jmpolitzer](https://github.com/jmpolitzer)
<file_sep>/src/useApiRequest/__mocks__/componentMock.js
import React from 'react'
import { useApiRequest } from '../../index'

/* eslint-disable-next-line react/prop-types */
function ComponentMock({ axios }) {
  const apiConfig = {
    axios: axios,
    key: 'testing',
    debug: true
  }
  const { state, makeApiRequest, makeApiRequests } = useApiRequest(apiConfig)
  const {
    fetching,
    resources: { things, thangs, thingThangs },
    errors
  } = state

  const thingsRequest = {
    things: {
      url: '/things'
    }
  }

  const multiRequest = {
    thangs: {
      url: '/thangs'
    },
    things: {
      url: '/things'
    }
  }

  const sequentialRequest = {
    thingThangs: {
      url: '/things/2',
      next: {
        url: '/thangs?thingId={{data.id}}'
      }
    }
  }

  return (
    <div>
      <button onClick={() => makeApiRequest(thingsRequest)}>Get Things</button>
      <button onClick={() => makeApiRequests(multiRequest)}>
        Get Multiple Resources
      </button>
      <button onClick={() => makeApiRequest(sequentialRequest)}>
        Get ThingThangs
      </button>

      {fetching.includes('things') && (
        <div data-testid='fetching-things'>fetching things</div>
      )}
      {things &&
        things.data.length > 0 &&
        things.data.map(thing => (
          <div data-testid='thing' key={thing.id}>
            {thing.text}
          </div>
        ))}
      {errors.things && (
        <div data-testid='things-error'>{errors.things.message}</div>
      )}

      {fetching.includes('thangs') && (
        <div data-testid='fetching-thangs'>fetching multiple resources</div>
      )}
      {thangs &&
        thangs.data.length > 0 &&
        thangs.data.map(thang => (
          <div data-testid='thang' key={thang.id}>
            {thang.text}
          </div>
        ))}
      {errors.thangs && (
        <div data-testid='thangs-error'>{errors.thangs.message}</div>
      )}

      {fetching.includes('thingThangs') && (
        <div data-testid='fetching-thingThangs'>fetching thingThangs</div>
      )}
      {thingThangs &&
        thingThangs.data.length > 0 &&
        thingThangs.data.map(thingThang => (
          <div data-testid='thingThang' key={thingThang.id}>
            {thingThang.text}
          </div>
        ))}
    </div>
  )
}

export default ComponentMock
<file_sep>/src/index.js
import useApiRequest from "./useApiRequest";

export { useApiRequest };
