@victorzimnikov/utility-hooks

Collection of low-level React hooks.

Usage no npm install needed!

<script type="module">
  import victorzimnikovUtilityHooks from 'https://cdn.skypack.dev/@victorzimnikov/utility-hooks';
</script>

README

utility-hooks

Collection of low-level React hooks.

Build codecov npm version npm minzipped size npm type definitions npm downloads npm license

Installation

npm install @victorzimnikov/utility-hooks

Environment compatibility

utility-hooks output uses modern browser features, all extra transpilations and polyfills should be done in application side.

Static checking with react-hooks/exhaustive-deps

 {
-  "react-hooks/exhaustive-deps": ["warn"]
+  "react-hooks/exhaustive-deps": [
+    "warn",
+    {
+      "additionalHooks": "^(useMemoWith|usePromise|usePureDeps|usePureMemo|useIsomorphicLayoutEffect)quot;
+    }
+  ]
 }

Hooks

useEventCallback(callback)

Inspired by How to read an often-changing value from useCallback?

Unlike useCallback, useEventCallback does not accept second argument and stores original callback in ref.

 function Form() {
   const [text, updateText] = useState("");
-  const textRef = useRef();
-
-  useEffect(() => {
-    textRef.current = text; // Write it to the ref
-  });
-
-  const handleSubmit = useCallback(() => {
-    const currentText = textRef.current; // Read it from the ref
-    alert(currentText);
-  }, [textRef]); // Don't recreate handleSubmit like [text] would do
+  const handleSubmit = useEventCallback(() => {
+    alert(text);
+  });

   return (
     <>
       <input value={text} onChange={e => updateText(e.target.value)} />
       <ExpensiveTree onSubmit={handleSubmit} />
     </>
   );
 }

useIsomorphicLayoutEffect(effect, deps)

Inspired by react-redux/src/utils/useIsomorphicLayoutEffect

Runs useLayoutEffect in browser environment (checks document.createElement), otherwise useEffect.

useConstant(factory)`

Inspired by How to create expensive objects lazily?

Runs factory only once and writes value in component ref.

 function Image(props) {
-  const ref = useRef(null);
   const node = useRef();
-
-  // ✅ IntersectionObserver is created lazily once
-  function getObserver() {
-    let observer = ref.current;
-    if (observer !== null) {
-      return observer;
-    }
-    let newObserver = new IntersectionObserver(onIntersect);
-    ref.current = newObserver;
-    return newObserver;
-  }
+  const observer = useConstant(() => new IntersectionObserver(onIntersect));

   useEffect(() => {
-    getObserver().observe(node.current);
+    observer.observe(node.current);
   }, [observer]);
 }

useMemoWith(factory, deps, isEqual)

Inspired by Gist.

Compares each dependency with isEqual function to memoize value from factory.

 export function useFetch(url, options) {
-  const cachedOptionsRef = useRef();
-
-  if (
-    !cachedOptionsRef.current ||
-    !_.isEqual(options, cachedOptionsRef.current)
-  ) {
-    cachedOptionsRef.current = options;
-  }
+  const cachedOptions = useMemoWith(() => options, [options], _.isEqual);

   useEffect(() => {
     // Perform fetch
-  }, [url, cachedOptionsRef.current]);
+  }, [url, cachedOptions]);
 }

usePrevious(value)

Inspired by How to get the previous props or state?

Stores value used in previous render.

 function Counter() {
-  const prevCountRef = useRef();
   const [count, setCount] = useState(0);
-
-  useEffect(() => {
-    prevCountRef.current = count;
-  });
+  const prevCount = usePrevious(count);

   return (
     <h1>
-      Now: {count}, before: {prevCountRef.current}
+      Now: {count}, before: {prevCount}
     </h1>
   );
 }

usePromise(factory, deps)

Handles loading of promises created by factory function.

const [filter, setFilter] = useState('')
- const [value, setValue] = useState();
- const [error, setError] = useState()
- useEffect(() => {
-   const controller = new AbortController();
-   const runEffect = async () => {
-     try {
-       const value = await fetch(
-         "https://foo.bars/api?filter=" + filter,
-         { signal: controller.signal }
-       );
-
-       setValue(value);
-     } catch (error) {
-       if (err.name === 'AbortError') {
-         console.log("Request was canceled via controller.abort");
-         return;
-       }
-
-       setError(error)
-     }
-   };
-   runEffect();
-   return () => controller.abort()
- }, [filter]);
+ const { value, error } = usePromise(({ abortSignal }) => fetch(
+  "https://foo.bars/api?filter=" + filter,
+   { signal: abortSignal }
+ ), [filter])

usePureMemo(deps, isEqual)

Returns next deps only when they were changed based on isEqual result.

usePureMemo(factory, deps, isEqual)

Works like useMemoWith, but also compares return value.

 export function useFetch(url, options) {
-  const cachedOptionsRef = useRef();
-
-  if (
-    !cachedOptionsRef.current ||
-    !_.isEqual(options, cachedOptionsRef.current)
-  ) {
-    cachedOptionsRef.current = options;
-  }
+  const cachedOptions = usePureMemo(() => options, [options], _.isEqual);

   useEffect(() => {
     // Perform fetch
-  }, [url, cachedOptionsRef.current]);
+  }, [url, cachedOptions]);
 }

useValueRef(value)

Inspired by How to read an often-changing value from useCallback?

Works like useRef, but keeps it's ref in sync with value on every call.

function Form() {
  const [text, updateText] = useState('');
+  const textRef = useValueRef(text);
-  const textRef = useRef();
-
- useEffect(() => {
-   textRef.current = text; // Write it to the ref
- });

  const handleSubmit = useCallback(() => {
    const currentText = textRef.current; // Read it from the ref
    alert(currentText);
  }, [textRef]); // Don't recreate handleSubmit like [text] would do

  return (
    <>
      <input value={text} onChange={e => updateText(e.target.value)} />
      <ExpensiveTree onSubmit={handleSubmit} />
    </>
  );
}

useWhenValueChanges(value, effect, isEqual)

Works like useEffect, but runs effect only when value compared by isEqual (Object.is if not provided). It also passes the previous value as an effect argument.

function List({ disptach, page, selectedId }) {
-  const isInitial = useRef(true);
  useEffect(() => {
-    isInitial.current = true;
    dispatch({ type: "FETCH_LIST", page });
  }, [page, dispatch]);
  useEffect(() => {
    dispatch({ type: "FETCH_ITEM", id: selectedId });
  }, [selectedId, dispatch]);
-  useEffect(() => {
-    if (isInitial.current) {
-      isInitial.current = false;
-    } else if (!selectedId) {
-      dispatch({ type: "FETCH_LIST", page });
-    }
-  }, [page, selectedId, dispatch]);
+  useWhenValueChanges(selectedId, () => {
+    if (!selectedId) {
+      dispatch({ type: "FETCH_LIST", page });
+    }
+  });
}

Utilities

areDepsEqualWith(hookName, nextDeps, prevDeps, isEqual)

Compares each dependency with isEqual function.