# Custom hooks

* [react-use](https://github.com/streamich/react-use)
* [beautiful-react-hooks](https://antonioru.github.io/beautiful-react-hooks/)
* [**useEventListener**](https://usehooks.com/useEventListener/)
* [**usePrevious**](https://usehooks.com/usePrevious/)
* Data Injection + [useAction](https://gist.github.com/Vlasakh/e7a79ff2cdfaa2768e30517097546ec9)
* [30 sec of code](https://github.com/30-seconds/30-seconds-of-code/tree/master)
  * [useTimeout](https://github.com/30-seconds/30-seconds-of-code/blob/v8.0.0/snippets/react/s/use-timeout.md)
  * [`useClickInside`](https://github.com/30-seconds/30-seconds-of-react#useclickinside)
  * [`useClickOutside`](https://github.com/30-seconds/30-seconds-of-react#useclickoutside)
  * [`useFetch`](https://github.com/30-seconds/30-seconds-of-react#usefetch)
  * [`useInterval`](https://github.com/30-seconds/30-seconds-of-react#useinterval)
  * [`useNavigatorOnLine`](https://github.com/30-seconds/30-seconds-of-react#usenavigatoronline)
  * [`useSSR`](https://github.com/30-seconds/30-seconds-of-react#usessr)
* [dev.to/react-custom-hook-usearray-1ogp](https://dev.to/sergeyleschev/react-custom-hook-usearray-1ogp) | [github.com/react-custom-hooks](https://github.com/sergeyleschev/react-custom-hooks)
  * useArray
  * useAsync
  * useClickOutside
  * useCookie
  * useCopyToClipboard
  * useDarkMode
  * useDebounce
  * useDebugInformation
  * useDeepCompareEffect
  * useEffectOnce
  * useEventListener
  * useFetch
  * useGeolocation
  * useHover
  * useLongPress
  * useMediaQuery
  * useOnlineStatus
  * useOnScreen
  * usePrevious
  * useRenderCount

## State

### functional setState

<details>

<summary>Code</summary>

```javascript
interface MyState {
   loading: boolean;
   data: any;
   something: string;
}

const [state, setState] = useReducer<Reducer<MyState, Partial<MyState>>>(
  (state, newState) => ({...state, ...newState}),
  {loading: true, data: null, something: ''}
)
```

</details>

### [useAsyncReference](https://css-tricks.com/dealing-with-stale-props-and-states-in-reacts-functional-components/)&#x20;

{% tabs %}
{% tab title="Info" %}
useState with a stable link
{% endtab %}

{% tab title="Hook" %}

```javascript
function useAsyncReference(value, isProp = false) {
  const ref = useRef(value);
  const [, forceRender] = useState(false);

  function updateState(newState) {
    if (!Object.is(ref.current, newState)) {
      ref.current = newState;
      forceRender(s => !s);
    }
  }

  if (isProp) {
    ref.current = value;
    return ref;
  }

  return [ref, updateState];
}
```

{% endtab %}

{% tab title="Usage" %}
{% code lineNumbers="true" %}

```javascript
function useAsyncReference(value) { ... }

function Counter() {
  const [count, setCount] = useAsyncReference(0);

  function handleAlertClick() {
    setTimeout(() => {
      alert("You clicked on: " + count.current);
    }, 3000);
  }

  return (
    <div>
      <p>You clicked {count.current} times</p>
      <button
        onClick={() => {
          setCount(count.current + 1);
        }}
      >
        Click me
      </button>
      <button onClick={handleAlertClick}>Show alert</button>
    </div>
  );
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

## Lifecicle

### useWillMount

<details>

<summary>Code</summary>

```javascript
function useWillMount(callback) {
  const ref = useRef();

  if (!ref.current) {
    ref.current = {
      value: callback()
    };
  }

  return ref.current.value;
}
```

</details>

## Closure

### [useEvent](https://codesandbox.io/s/useevent-3g8el4?file=/src/hooks/useEvent.js:340-356) (useCallback analog)&#x20;

{% tabs %}
{% tab title="Usage" %}
{% code lineNumbers="true" %}

```javascript
function App() {
  const [text, setText] = useState("");

  const onClick = useEvent(() => { // <== stable link
    console.log("test:", text);
  });
}
```

{% endcode %}
{% endtab %}

{% tab title="Info" %}
RFC <https://github.com/reactjs/rfcs/blob/useevent/text/0000-useevent.md>
{% endtab %}
{% endtabs %}

### [useDeepCompareEffect](https://codesandbox.io/s/usedeepcompareeffect-f3qj9y) (useEffect)

<details>

<summary>Code</summary>

{% code lineNumbers="true" %}

```javascript
function App() {
  const [count, setCount] = useState(0);
  const [counterObj, setCounterObj] = useState({ firstCount: 0 });
  const [secondObj, setSecondObj] = useState({ secCount: 0 });

  useDeepCompareEffect(() => {
    console.log("counterObj", [count], counterObj, secondObj);
  }, [counterObj]);
}
```

{% endcode %}

</details>

## Forms

### useInput

<details>

<summary>Code</summary>

{% code lineNumbers="true" %}

```javascript
export default function useInput(initialValue) {
    const [value, setValue] = useState(initialValue);

    const onChange = e => {
        setValue(e.target.value)
    }

    return {
        value, onChange
    }
};
```

{% endcode %}

</details>

## Network, geo

### useLocation

{% tabs %}
{% tab title="Info" %}
предоставляет данные о геолокации в реальном времени, а также метрики курса движения и скорости. 🗺️📍  Демо: <https://grayscal.es/hooks/use-location>

Используйте его для получения информации о близлежащих местах или для определения местоположения пользователей поблизости.&#x20;
{% endtab %}

{% tab title="Code" %}
<https://gist.github.com/KristofferEriksson/ee5af0a52e1fea0acc028e9b10aa0969>

```typescript
import { useEffect, useState } from "react";

interface LocationOptions {
  enableHighAccuracy?: boolean;
  timeout?: number;
  maximumAge?: number;
}

interface LocationState {
  coords: {
    latitude: number | null;
    longitude: number | null;
    accuracy: number | null;
    altitude: number | null;
    altitudeAccuracy: number | null;
    heading: number | null;
    speed: number | null;
  };
  locatedAt: number | null;
  error: string | null;
}

const useLocation = (options: LocationOptions = {}) => {
  const [location, setLocation] = useState<LocationState>({
    coords: {
      latitude: null,
      longitude: null,
      accuracy: null,
      altitude: null,
      altitudeAccuracy: null,
      heading: null,
      speed: null,
    },
    locatedAt: null,
    error: null,
  });

  useEffect(() => {
    // Ensuring this runs only in a client-side environment
    if (typeof window === "undefined" || !("geolocation" in navigator)) {
      setLocation((prevState) => ({
        ...prevState,
        error:
          "Geolocation is not supported by your browser or not available in the current environment",
      }));
      return;
    }

    const handleSuccess = (position: GeolocationPosition) => {
      setLocation({
        coords: {
          latitude: position.coords.latitude,
          longitude: position.coords.longitude,
          accuracy: position.coords.accuracy,
          altitude: position.coords.altitude,
          altitudeAccuracy: position.coords.altitudeAccuracy,
          heading: position.coords.heading,
          speed: position.coords.speed,
        },
        locatedAt: position.timestamp,
        error: null,
      });
    };

    const handleError = (error: GeolocationPositionError) => {
      setLocation((prevState) => ({
        ...prevState,
        error: error.message,
      }));
    };

    const geoOptions = {
      enableHighAccuracy: options.enableHighAccuracy || false,
      timeout: options.timeout || Infinity,
      maximumAge: options.maximumAge || 0,
    };

    const watcher = navigator.geolocation.watchPosition(
      handleSuccess,
      handleError,
      geoOptions,
    );

    return () => navigator.geolocation.clearWatch(watcher);
  }, [options.enableHighAccuracy, options.timeout, options.maximumAge]);

  return location;
};

export default useLocation;
```

{% endtab %}
{% endtabs %}

### useNetwork

{% tabs %}
{% tab title="Info" %}
Вы можете узнать скорость передачи данных, тип соединения и статус сети прямо в вашем приложении.  Demo: <https://grayscal.es/hooks/use-network>
{% endtab %}

{% tab title="Code" %}
<https://gist.github.com/KristofferEriksson/7e3de25dfa587943abc04841f57d635c>

```typescript
import { useEffect, useState } from "react";

interface NetworkInformation extends EventTarget {
  downlink?: number;
  effectiveType?: "slow-2g" | "2g" | "3g" | "4g";
  rtt?: number;
  saveData?: boolean;
  onchange?: EventListener;
}

interface NavigatorWithNetworkInformation extends Navigator {
  connection?: NetworkInformation;
}

interface NetworkState {
  downlink?: number | null;
  effectiveType?: "slow-2g" | "2g" | "3g" | "4g" | null;
  rtt?: number | null;
  saveData?: boolean | null;
  isOnline: boolean;
}

function useNetwork(): NetworkState {
  const [networkState, setNetworkState] = useState<NetworkState>({
    downlink: null,
    effectiveType: null,
    rtt: null,
    saveData: null,
    isOnline: false, // Default to false; updated correctly on the client-side
  });

  useEffect(() => {
    // Ensure we are in the browser environment
    if (typeof window === "undefined") {
      return;
    }

    const nav = navigator as NavigatorWithNetworkInformation;

    if (!nav.connection) {
      setNetworkState((prevState) => ({
        ...prevState,
        downlink: null,
        effectiveType: null,
        rtt: null,
        saveData: null,
        isOnline: navigator.onLine, // Update online status in the browser
      }));
      return;
    }

    const updateNetworkInfo = () => {
      const { downlink, effectiveType, rtt, saveData } = nav.connection!;
      setNetworkState((prevState) => ({
        ...prevState,
        downlink,
        effectiveType,
        rtt,
        saveData,
        isOnline: navigator.onLine,
      }));
    };

    updateNetworkInfo();
    nav.connection!.addEventListener("change", updateNetworkInfo);
    window.addEventListener("online", () =>
      setNetworkState((prevState) => ({ ...prevState, isOnline: true })),
    );
    window.addEventListener("offline", () =>
      setNetworkState((prevState) => ({ ...prevState, isOnline: false })),
    );

    return () => {
      if (nav.connection) {
        nav.connection.removeEventListener("change", updateNetworkInfo);
      }
      window.removeEventListener("online", () =>
        setNetworkState((prevState) => ({ ...prevState, isOnline: true })),
      );
      window.removeEventListener("offline", () =>
        setNetworkState((prevState) => ({ ...prevState, isOnline: false })),
      );
    };
  }, []);

  return networkState;
}

export default useNetwork;
```

{% endtab %}
{% endtabs %}

## Misc

### useExternalScript

{% tabs %}
{% tab title="Usage" %}

```javascript
  const { isLoading, isError } = useExternalScript({
    url,
    onSuccess: () => . . . do smth . . .
  });
```

{% endtab %}

{% tab title="Source" %}

```javascript
import { useEffect, useState } from 'react';

type Hook = (props: { url: string; onSuccess?: () => void }) => { isLoading: boolean; isError: boolean };

export const useExternalScript: Hook = ({ url, onSuccess }) => {
  const [isLoading, setIsLoading] = useState(!!url);
  const [isError, setI  const { isLoading: isScriptLoading, isError: isScriptLoadingError } = useExternalScript({
    url,
    onSuccess: () => window.encryptData || setIsError(true),
  });sError] = useState(false);

  useEffect(() => {
    if (!url) {
      return;
    }

    let script: HTMLScriptElement | null = document.querySelector(`script[src="${url}"]`);

    if (script) {
      setIsLoading(false);
      return;
    }

    const handleScript = (e: Event) => {
      if (e.type === 'load') {
        setIsLoading(false);
        onSuccess && onSuccess();
      }
      e.type !== 'load' && setIsError(true);
    };

    script = document.createElement('script');
    script.type = 'application/javascript';
    script.src = url;
    script.setAttribute('data-testid', 'external-script');
    script.async = true;
    document.body.appendChild(script);
    script.addEventListener('load', handleScript);
    script.addEventListener('error', handleScript);

    script.addEventListener('load', handleScript);
    script.addEventListener('error', handleScript);

    return () => {
      script?.removeEventListener('load', handleScript);
      script?.removeEventListener('error', handleScript);
    };
  }, [onSuccess, url]);

  return { isLoading, isError };
};

```

{% endtab %}
{% endtabs %}

### useScroll

<details>

<summary>Code</summary>

{% code lineNumbers="true" %}

```javascript
export default function useScroll(parentRef, childRef, callback) {
    const observer = useRef();

    useEffect(() => {
        const options = {
            root: parentRef.current,
            rootMargin: '0px',
            threshold: 0
        }
        observer.current = new IntersectionObserver(([target]) => {
            if (target.isIntersecting) {
                console.log('intersected')
                callback()
            }
        }, options)

        observer.current.observe(childRef.current)

        return function () {
            observer.current.unobserve(childRef.current)
        };
    }, [callback])
};
```

{% endcode %}

</details>

### Selection

#### useTextSelection

{% tabs %}
{% tab title="useTextSelection" %}
<https://t.me/react_tg/417> отслеживает выделение текста пользователем и его положение на экране!  Демо: <http://grayscal.es/hooks/use-text-selection>

Идеально подходит для всплывающая подсказка "Поделиться", подобно Medium. ✨&#x20;
{% endtab %}

{% tab title="Code" %}
<https://gist.github.com/KristofferEriksson/8acb9b3eb241507eb0f6232938bf4ec7>

```typescript
import { useEffect, useState } from "react";

type UseTextSelectionReturn = {
  text: string;
  rects: DOMRect[];
  ranges: Range[];
  selection: Selection | null;
};

const getRangesFromSelection = (selection: Selection): Range[] => {
  const rangeCount = selection.rangeCount;
  return Array.from({ length: rangeCount }, (_, i) => selection.getRangeAt(i));
};

const isSelectionInsideRef = (
  selection: Selection,
  ref: React.RefObject<HTMLElement>,
) => {
  if (!ref.current || selection.rangeCount === 0) return true;

  const range = selection.getRangeAt(0);
  return ref.current.contains(range.commonAncestorContainer);
};

export function useTextSelection(
  elementRef?: React.RefObject<HTMLElement>,
): UseTextSelectionReturn {
  const [selection, setSelection] = useState<Selection | null>(null);
  const [text, setText] = useState("");
  const [ranges, setRanges] = useState<Range[]>([]);
  const [rects, setRects] = useState<DOMRect[]>([]);

  useEffect(() => {
    const onSelectionChange = () => {
      const newSelection = window.getSelection();

      if (
        newSelection &&
        (!elementRef || isSelectionInsideRef(newSelection, elementRef))
      ) {
        setSelection(newSelection);
        setText(newSelection.toString());
        const newRanges = getRangesFromSelection(newSelection);
        setRanges(newRanges);
        setRects(newRanges.map((range) => range.getBoundingClientRect()));
      } else {
        setText("");
        setRanges([]);
        setRects([]);
        setSelection(null);
      }
    };

    document.addEventListener("selectionchange", onSelectionChange);

    return () => {
      document.removeEventListener("selectionchange", onSelectionChange);
    };
  }, [elementRef]);

  return {
    text,
    rects,
    ranges,
    selection,
  };
}
```

{% endtab %}
{% endtabs %}
