useHooks


This hook allows you to detect clicks outside of a specified element. In the example below we use it to close a modal when any element outside of the modal is clicked. By abstracting this logic out into a hook we can easily use it across all of our components that need this kind of functionality (dropdown menus, tooltips, etc).

import { useState, useEffect, useRef } from 'react'; // Usage function App() { // Create a ref that we add to the element for which we want to detect outside clicks const ref = useRef(); // State for our modal const [isModalOpen, setModalOpen] = useState(false); // Call hook passing in the ref and a function to call on outside click useOnClickOutside(ref, () => setModalOpen(false)); return ( <div> {isModalOpen ? ( <div ref={ref}> 👋 Hey, I'm a modal. Click anywhere outside of me to close. </div> ) : ( <button onClick={() => setModalOpen(true)}>Open Modal</button> )} </div> ); } // Hook function useOnClickOutside(ref, handler) { useEffect(() => { const listener = event => { // Do nothing if clicking ref's element or descendent elements if (!ref.current || ref.current.contains(event.target)) { return; } handler(event); }; document.addEventListener('mousedown', listener); document.addEventListener('touchstart', listener); return () => { document.removeEventListener('mousedown', listener); document.removeEventListener('touchstart', listener); }; }, []); // Empty array ensures that effect is only run on mount and unmount }

November 5th, 2018

This hook allows you to smoothly animate any value using an easing function (linear, elastic, etc). In the example we call the useAnimation hook three times to animated three balls on to the screen at different intervals. Additionally we show how easy it is to compose hooks. Our useAnimation hook doesn't actual make use of useState or useEffect itself, but instead serves as a wrapper around the useAnimationTimer hook. Having the timer logic abstracted out into its own hook gives us better code readability and the ability to use timer logic in other contexts. Be sure to check out the CodeSandbox Demo for this one.
import { useState, useEffect } from 'react'; // Usage function App() { // Call hook multiple times to get animated values with different start delays const animation1 = useAnimation('elastic', 600, 0); const animation2 = useAnimation('elastic', 600, 150); const animation3 = useAnimation('elastic', 600, 300); return ( <div style={{ display: 'flex', justifyContent: 'center' }}> <Ball innerStyle={{ marginTop: animation1 * 200 - 100 }} /> <Ball innerStyle={{ marginTop: animation2 * 200 - 100 }} /> <Ball innerStyle={{ marginTop: animation3 * 200 - 100 }} /> </div> ); } const Ball = ({ innerStyle }) => ( <div style={{ width: 100, height: 100, marginRight: '40px', borderRadius: '50px', backgroundColor: '#4dd5fa', ...innerStyle }} /> ); // Hook  function useAnimation( easingName = 'linear', duration = 500, delay = 0 ) { // The useAnimationTimer hook calls useState every animation frame ... // ... giving us elapsed time and causing a rerender as frequently ... // ... as possible for a smooth animation. const elapsed = useAnimationTimer(duration, delay); // Amount of specified duration elapsed on a scale from 0 - 1 const n = Math.min(1, elapsed / duration); // Return altered value based on our specified easing function return easing[easingName](n); } // Some easing functions copied from: // https://github.com/streamich/ts-easing/blob/master/src/index.ts // Hardcode here or pull in a dependency const easing = { linear: n => n, elastic: n => n * (33 * n * n * n * n - 106 * n * n * n + 126 * n * n - 67 * n + 15), inExpo: n => Math.pow(2, 10 * (n - 1)) }; function useAnimationTimer(duration = 1000, delay = 0) { const [elapsed, setTime] = useState(0); useEffect( () => { let animationFrame, timerStop, start; // Function to be executed on each animation frame function onFrame() { setTime(Date.now() - start); loop(); } // Call onFrame() on next animation frame function loop() { animationFrame = requestAnimationFrame(onFrame); } function onStart() { // Set a timeout to stop things when duration time elapses timerStop = setTimeout(() => { cancelAnimationFrame(animationFrame); setTime(Date.now() - start); }, duration); // Start the loop start = Date.now(); loop(); } // Start after specified delay (defaults to 0) const timerDelay = setTimeout(onStart, delay); // Clean things up return () => { clearTimeout(timerStop); clearTimeout(timerDelay); cancelAnimationFrame(animationFrame); }; }, [duration, delay] // Only re-run effect if duration or delay changes ); return elapsed; }

November 2nd, 2018

A really common need is to get the current size of the browser window. This hook returns an object containing the window's width and height. If executed server-side (no window object) the value of width and height will be undefined.

import { useState, useEffect } from 'react'; // Usage function App() { const size = useWindowSize(); return ( <div> {size.width}px / {size.height}px </div> ); } // Hook function useWindowSize() { const isClient = typeof window === 'object'; function getSize() { return { width: isClient ? window.innerWidth : undefined, height: isClient ? window.innerHeight : undefined }; } const [windowSize, setWindowSize] = useState(getSize); function handleResize() { setWindowSize(getSize()); } useEffect(() => { if (!isClient) { return false; } window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); // Empty array ensures that effect is only run on mount and unmount return windowSize; }

October 31st, 2018

Detect whether the mouse is hovering an element. The hook returns a ref and a boolean value indicating whether the element with that ref is currently being hovered. So just add the returned ref to any element whose hover state you want to monitor.

 import { useRef, useState, useEffect } from 'react'; // Usage function App() { const [hoverRef, isHovered] = useHover(); return ( <div ref={hoverRef}> {isHovered ? '😁' : '☹️'} </div> ); } // Hook function useHover() { const [value, setValue] = useState(false); const ref = useRef(null); const handleMouseOver = () => setValue(true); const handleMouseOut = () => setValue(false); useEffect( () => { const node = ref.current; if (node) { node.addEventListener('mouseover', handleMouseOver); node.addEventListener('mouseout', handleMouseOut); return () => { node.removeEventListener('mouseover', handleMouseOver); node.removeEventListener('mouseout', handleMouseOut); }; } }, [ref.current] // Recall only if ref changes ); return [ref, value]; }

October 30th, 2018

Sync state to local storage so that it persists through a page refresh. Usage is similar to useState except we pass in a local storage key so that we can default to that value on page load instead of the specified initial value.

import { useState, useEffect } from 'react'; // Usage function App() { // Similar to useState but we pass in a key to value in local storage // With useState: const [name, setName] = useState('Bob'); const [name, setName] = useLocalStorage('name', 'Bob'); return ( <div> <input type="text" placeholder="Enter your name" value={name} onChange={e => setName(e.target.value)} /> </div> ); } // Hook function useLocalStorage(key, initialValue) { // The initialValue arg is only used if there is nothing in localStorage ... // ... otherwise we use the value in localStorage so state persist through a page refresh. // We pass a function to useState so localStorage lookup only happens once. // We wrap in try/catch in case localStorage is unavailable const [item, setInnerValue] = useState(() => { try { return window.localStorage.getItem(key) ? JSON.parse(window.localStorage.getItem(key)) : initialValue; } catch (error) { // Return default value if JSON parsing fails return initialValue; } }); // Return a wrapped version of useState's setter function that ... // ... persists the new value to localStorage. const setValue = value => { setInnerValue(value); window.localStorage.setItem(key, JSON.stringify(item)); }; // Alternatively we could update localStorage inside useEffect ... // ... but this would run every render and it really only needs ... // ... to happen when the returned setValue function is called. /* useEffect(() => { window.localStorage.setItem(key, JSON.stringify(item)); }); */ return [item, setValue]; }

October 29th, 2018