npm package: @focus21/hooks
Description: Focus21's React hooks.
Author: Focus21
Latest version: 8.7.1

# @focus21/hooks

Focus21's React hooks.

## Usage

### Installation

```bash
pnpm add --save @focus21/hooks
```

## Hooks

### getUseFetch

Get data using [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch).

**Setup example**

```javascript
// utils/useFetch.js

// You must provide a library that implements the fetch API, such as `unfetch`.
// https://github.com/developit/unfetch
import unfetch from "unfetch";

import { getUseFetch } from "@focus21/hooks";

const useFetch = getUseFetch(unfetch);

export default useFetch;
```

**Get request example**

```javascript
// utils/hello.js

import useFetch from "./useFetch";

// Example simple get request hook.
const useGetHello = () =>
  useFetch("/hello", {
    fetchOnRender: true,
  });

export { useGetHello };
```

```javascript
// pages/index.jsx

import React, { useEffect } from "react";

import { useGetHello } from "../utils/hello";

const Page = () => {
  const getHello = useGetHello();

  // Indicate that there was an error.
  // You probably want a proper component for showing error messages.
  //
  // Talk to UX to ask about how to show errors in your app.
  //
  // Avoid "toaster" errors since they are easy to miss and hard to make accessible.
  if (getHello.error) {
    return <div>Error!</div>;
  }

  // Indicate that data is loading.
  // You probably want a proper loading indicator component.
  //
  // Talk to UX to ask about how to indicate loading in your app.
  if (!getHello.isLoaded) {
    return <div>Loading...</div>;
  }

  // This is not a good way to display data in a real page, just an example
  return <div>Data: {JSON.stringify(getHello.data)}</div>;
};
```

**Set defaults example**

```javascript
// utils/ping

import useFetch from "./useFetch";

const usePostPing = () =>
  useFetch("/ping", {
    // Any fetch options are supported by "defaults", plus a few of our own helper options.
    defaults: {
      method: "POST",
    },
  });

export { usePostPing };
```

**Override defaults example**

```javascript
// utils/foo

import useFetch from "./useFetch";

const useFoo = () => useFetch("/foo");

export { useFoo };
```

```javascript
// pages/index.jsx

import React, { useEffect } from "react";

import { useFoo } from "../utils/hello";

const Page = () => {
  const foo = useFoo();

  // Any of the defaults set by "useFetch" can be overridden on "doFetch".
  // The URL itself can also be overwritten if the first argumetn is set.
  return (
    <div>
      <div onClick={() => foo.doFetch(null, { method: "GET" })}>
        Click me to get foo.
      </div>
      <div onClick={() => foo.doFetch(null, { method: "POST" })}>
        Click me to post foo.
      </div>
    </div>
  );
};
```

**Payload example**

```javascript
// utils/hello.jsx

import { useEffect, useMemo } from "react";

import useFetch "./useFetch";

// The payload should be memoized (using React's useMemo) for optimal results.
const useGetHelloQuery = (payload) => useFetch("/hello", {
  // Use "options.defaults.payload" to automatically encode the query string for GET requests.
  // If you wish to encode it yourself, use the URL instead.
  // "options.payload" can be used when calling "doFetch" to override this default.
  defaults: { payload },
});

const usePostHelloQuery = (payload) => useFetch("/hello", {
  defaults: {
    method: "POST",
    // Use "options.defaults.payload" to automatically encode the body for POST, PUT, and PATCH requests.
    // If you wish to encode it yourself, use "options.body" insted.
    // "options.payload" can be used when calling "doFetch" to override this default.
    payload,
  },
});

export { useGetHelloQuery, usePostHelloQuery };
```

**Save last successful data example**

```javascript
// pages/index.jsx

import React, { useEffect, useState } from "react";

import { useGetHello } from "../utils/user";

const Page = () => {
  const getHello = useGetHello();

  useEffect(() => {
    if (!getHello.initialized) {
      getHello.doFetch();
    }
  }, [getHello]);

  const [lastData, setLastData] = useState(getHello.data);
  useEffect(() => {
    if (getHello.data && !getHello.error) {
      setLastData(getHello.data);
    }
  }, [getHello]);

  if (!lastData && getHello.error) {
    return <div>Error!</div>;
  }

  if (!getHello.isLoaded) {
    return <div>Loading...</div>;
  }

  return (
    <>
      <div>Data: {JSON.stringify(lastData)}</div>
      {getHello.isFetching ? <div>Loading...</div> : null}
      {getHello.error ? <div>Error!</div> : null}
    </>
  );
};
```

**Shared data and mutations example**

Use [context](https://reactjs.org/docs/context.html) to share data and trigger mutations.

```javascript
// utils/user.js

import React, { createContext, useContext } from "react";

const useGetUser = () => useFetch("/user");

const UserContext = createContext({});

const UserProvider = ({ children }) => {
  const getUser = useGetUser();

  return (
    <UserContext.Provider value={getUser}>{children}</UserContext.Provider>
  );
};

const useUser = () => useContext(UserContext);

export { useGetUser, UserContext, UserProvider, useUser };
```

```javascript
// pages/_app.jsx

import React from "react";

import { UserProvider } from "../utils/user";

const App = ({ children }) => <UserProvider>{children}</UserProvider>;

export default App;
```

```javascript
// pages/index.jsx

import React from "react";

import { useUser } from "../utils/user";

const useGetUser = () => useFetch("/user");

// Add the context to any pages or components which use or mutate it.
const Page = () => {
  // The shared user can be referred to with `user.data`, `user.error`, etc.
  const user = useUser();

  // Refetch the user whenever there is a mutation.
  const doMutation = () => {
    user.doFetch();
  };

  // ...
};
```

### useDebounce

Debounce a value.

**Example**

```javascript
import React, { useState } from "react";

import { useDebounce } from "@focus21/hooks";

import useGetSearchResults from "./useGetSearchResults";

const Search = () => {
  const [search, setSearch] = useState("");

  // Debounce every half second
  const debouncedSearch = useDebounce(() => search, [search], 500);

  const searchResults = useGetSearchResults();

  useEffect(() => {
    if (debouncedSearch) {
      searchResults.doFetch(null, {
        payload: {
          search: debouncedSearch,
        },
      });
    }
  }, [debouncedSearch]);

  let results = null;
  if (search && searchResults.data) {
    results = searchResults.data.map(({ id, name }) => (
      <div key={id}>{name}</div>
    ));
  }

  return (
    <>
      <input type="text" value={search} />
      {results}
    </>
  );
};

export default Search;
```