DEV Community

Aleksandr Churbakov
Aleksandr Churbakov

Posted on

My React Hooks

React is dominating Frontend development and no wonder more than a half of applications being written are using it. The simplicity and strength is powered by tons of great features and React Hooks is one of them. As a Frontend developer I've been writing and using hooks almost every day of my career for the last couple of years. A lot of the hooks I've used were quite universal so I've spotted them migrating from one of my project to another without almost any change.

I'm not the only developer who has noticed that and you can easily find tons of React Hooks on npm, but introducing an additional dependency for a thing that takes less than 30 lines of code is a bit of a controversial decision, so over time I naturally have been choosing to write them myself. My collection grew and became quite big and useful, so here I am to share.

I'm still not making a library of it and treat the following code more as of a snippet rather than a package, cherry pick hooks I need and copy them to my project. That way I avoid useless complexity of either importing extra 3 to 5 libraries or maintaining a full size npm package while having a flexibility to edit the hooks the way my project may require - those are 10-20 liners in the end of day.

useLocalStorage

export const useLocalStorage = <T,>(key: string, def: T) => {
    const [value, setValue] = React.useState(JSON.parse(localStorage.getItem(key) || JSON.stringify(def)) as T);

    React.useEffect(() => localStorage.setItem(key, JSON.stringify(value)), [value]);

    return [value, setValue] as const;
};
Enter fullscreen mode Exit fullscreen mode

Pretty straightforward and simple hooks that you can find almost everywhere. You can change it to be somewhat durable by providing a "safer" version of JSON.parse or switch to session storage (or even cookies using universal-cookies get and set method).

useQueryParam(s)

export const useQueryParam = <T>(name: string) => {
    const params = new URLSearchParams(window.location.search);
    const value: T = params.get(name) || null;

    const setValue = (v) => {
        const params = new URLSearchParams(window.location.search);

        if (v) {
            params.set(name, v);
        } else {
            params.delete(name);
        }

        window.location.search = params.toString();
    };

    return [value, setValue] as const;
};
Enter fullscreen mode Exit fullscreen mode

Another simple one, usually I keep it being split in 2 hooks: useQueryParams (meaning all the params) and useQueryParam(name), but here I list a compiled version.

useEventListener

export const useListener = (target: any, event: string, callback: EventListenerOrEventListenerObject, deps: React.DependencyList) => {
    React.useEffect(() => {
        target.addEventListener(event, callback);
        return () => target.removeEventListener(event, callback);
    }, [...deps, callback]);
};
Enter fullscreen mode Exit fullscreen mode

This one is easy too, you can update this hook to include options that you can pass to addEventListener method. Usually I use this one to add listener to window. Don't forget to wrap callback into React.useCallback!

useForceUpdate

export const useForceUpdate = ([_, setState] = React.useState(false)) => 
    React.useCallback(() => setState(s => !s), [setState]);
Enter fullscreen mode Exit fullscreen mode

The smallest, but quite an important one. Usually I wrap this into const ticker = useTicker() hook and force container update on POST/PUT/DELETE actions (will explain in details later).

useClickOutside

const useClickOutside = (predicate) => {
    const ref = React.useRef(null);

    useListener(window, 'click', (e) => {
        if (!ref.current.contains(e.target)) {
            predicate();
        }
    }, [predicate]);

    return ref;
};
Enter fullscreen mode Exit fullscreen mode

That one returns a ref that, being attached to an element, will cause any click outside of this element to call predicate.

<Modal ref={useClickOutside(onClose)}>{...}</Modal>

useDebouncedEffect

Imagine you need to save user's Figma file when it's updated, but you don't want to do this every time something changes. You probably want to debounce update calls:

const useDebouncedEffect = (predicate, time, deps) => {
    const timeoutRef = React.useRef(null);

    React.useEffect(() => {
        if (timeoutRef.current) {
            clearTimeout(timeoutRef.current);
        }

        timeoutRef.current = setTimeout(predicate, time);
    }, deps);
};
Enter fullscreen mode Exit fullscreen mode

This one will only call your effect once in a time. Please note that predicate is not a part of deps and you probably want to change that, but, if you do, remember to wrap it with React.useCallback!

useDebouncedEffect(() => api.patchProfile({ name }), 500, [name]);

return (<TextInput value={name} onChange={setName} />);
Enter fullscreen mode Exit fullscreen mode

useRefCallback

Sometimes you want to do something with a delay and use setTimeout like I did in example above or perform an async call and call some function after. Sometimes that can cause bugs because inside timeout/promise handler you might be calling an "old version" of whatever function you wanted to call. These kinds of bugs are quite tricky to catch, but if you've found one, you can use the following hook:

const useRefCallback = (callback) => {
  const ref = React.useRef(null);

  React.useEffect(() => {
    ref.current = callback;
  }, [callback]);

  return ref;
}
Enter fullscreen mode Exit fullscreen mode

Using this instead (together is fine too actually) of useCallback you can get a fresh version of your callback any time you call it. But you will have to access it with .current.

usePreviousValue

Following up the hook above comes the previous value container.

const usePreviousValue = (value) => {
    const ref = React.useRef(value);
    const pref = React.useRef(value);

    React.useEffect(() => {
        pref.current = ref.current;
        ref.current = value;
    }, [value])

    return pref.current; // SIC!
};
Enter fullscreen mode Exit fullscreen mode

This hook will return the previous value of (you guessed it) value. I actually like to return ref instead of a value itself to highlight the nature of previous value and remove an incentive to hook an effect to it. This isn't forbidden, but it's better not to do so and listen to original value instead.

useInput & useValidate

Sometimes you may find yourself in a position of making a form for a simple input or two and not willing to bring in formik artillery or whatever form library you are into.

const useInput = <T,>(def: T) => {
    const [state, setState] = React.useState(def);

    return { value: state, onChange: setState } as Input<T>;
};

const useValidate = <T,>(input: Input<T>, ...validators: Validator<T>[]) => {
    const [error, setError] = React.useState(null);

    React.useEffect(() => {
        for (let validator of validators) {
            const error = validator(input.value);

            if (error) {
                setError(error);
                return;
            }
        }

        setError(null);
    }, [input.value]);

    return error;
};
Enter fullscreen mode Exit fullscreen mode

Those 2 usually go together and let me easily create simple forms like this:

const require = ($) => !$ && 'Required field';
const min = (n) => ($) => $.length < n && `Should have at least ${n} characters`;

const name = useInput('');

return (
  <Input {...name} error={useValidate(name, required, min(5))} />
);
Enter fullscreen mode Exit fullscreen mode

useAsyncMemo

Quite a popular one, you may have seen that as react-query used with GraphQL a lot. This is the essence of it:

export const useAsyncMemo = <T,>(predicate: Predicate<[], Promise<T>>, deps: React.DependencyList) => {
    const [loading, setLoading] = React.useState(false);
    const [value, setValue] = React.useState(null as T | null);
    const [error, setError] = React.useState(null as Error | null);

    React.useEffect(() => {
        setLoading(true);

        predicate().then((value) => {
            setValue(value);
        }).catch((error) => {
            setError(error);
        }).then(() => {
            setLoading(false);
        });
    }, deps);

    return [value, loading, error] as const;
};
Enter fullscreen mode Exit fullscreen mode

Please again note that deps don't include callback, but if you add it here, wrap it with useCallback.

useObservable

If you happen to use rxjs with your React project (and they get along really well), you can use the following code as a replacement for async pipe:

export const useObservable = (o) => {
    const [value, setValue] = React.useState(null);

    React.useEffect(() => {
      let sub = o.subscribe((value) => setValue(value);
      return () => sub.unsubscribe();
    }), [r]);

    return value;
};
Enter fullscreen mode Exit fullscreen mode

useWorker

Very simple and effective hook to delegate computation heavy tasks to a separate process.

const useWorker = (handler, callback) => {
  const workerRef = React.useRef(null);

  React.useEffect(() => {
    const worker = new Worker(URL.createObjectURL(
      new Blob([
        `onmessage=${handler}`
      ], { type: 'text/javascript' })
    ));

    worker.onmessage=callback;
    workerRef.current = worker;

    return () => worker.terminate();
  }, [handler, callback]);

  return workerRef;
};
Enter fullscreen mode Exit fullscreen mode

useBetween & useTicker

Finally, my favorite one. It's a separate npm library written by a great guy @betula that you can find here https://github.com/betula/use-between. It's a great replacement for all the useContext mess on my pet projects.

const useTicker = ([state, setState] = React.useState(false)) => React.useMemo(() => ({ update: () => setState($ => !$) }, [setState, state]);

const _useAuth = () => {
  const ticker = useTicker();
  const token = useLocalStorage('token', null);
  const [user] = useAsyncMemo(() => api.session.get(token), [token, ticker]);

  const updateName = React.useCallback((name) => {
    api.session.patch(token, { name })
      .then(ticker.update)
      .catch(/* notify */);
  }, [token]);

  return { user, updateName };
};

const useAuth = () => useBetween(_useAuth);
Enter fullscreen mode Exit fullscreen mode

That's how I've been designing my shared state and methods ever since I've found this great library. Any React Component using useAuth hook will receive the same data and it will be up to date as soon as you update user name.

And it's all I wanted to share. Feel free to post any cool React Hooks you know in the comments below.

Top comments (0)