Hooks is a new feature of React 16.8 that allows you to use state and other React functions without using class. This article provides easy-to-understand examples to help you understand how hooks are used and encourage you to use them in future projects. But before that, make sure you've read the hook's official documentation.
useEventListener
If you find yourself adding a lot of event listeners using useEffect, you may need to consider encapsulating these logic as a generic hook. In the following tips, we create a hook called useEventListener, which checks whether addEventListener is supported, adds event listeners, and clears listeners in the cleanup hook. You can view online examples on CodeSandbox demo.
import { useRef, useEffect, useCallback } from 'react';// Use function App(){ // State used to store mouse position const [coords, setCoords] = useState({ x: 0, y: 0 }); // Use useCallback to handle callbacks // ... Dependence will not change here. const handler = useCallback( ({ clientX, clientY }) => { // Update coordinates setCoords({ x: clientX, y: clientY }); }, [setCoords] ); // Adding events using custom hook s useEventListener('mousemove', handler); return ( <h1> The mouse position is ({coords.x}, {coords.y}) </h1> );}// Hookfunction useEventListener(eventName, handler, element = global){ // Create a ref for the storage method const savedHandler = useRef(); // Update ref.current's method when processing function changes // This allows us to always get the latest processing functions. // And it does not need to be passed in its effect dependency array // And avoid the possibility of re-eliciting the effect method each time rendering useEffect(() => { savedHandler.current = handler; }, [handler]); useEffect( () => { // Verify that addEventListener is supported const isSupported = element && element.addEventListener; if (!isSupported) return; // Create an event listener that calls functions stored in ref const eventListener = event => savedHandler.current(event); // Adding event listeners element.addEventListener(eventName, eventListener); // In cleanup callbacks, clear event listeners return () => { element.removeEventListener(eventName, eventListener); }; }, [eventName, element] // Rerun when the element or binding event changes );};
donavon/use-event-listener - This library can be used as the original resource for this hook.
useWhyDidYouUpdate
This hook makes it easier for you to see which prop changes resulted in a component's re-rendering. If a function runs once at a very high cost, and you know which props cause duplicate rendering, you can use React.memo as a high-order component to solve this problem, and then a Counter component will use this feature. In this case, if you're still looking for some seemingly unnecessary re-rendering, you can use the useWhyDidYouUpdate hook and check your console to see which prop changed during the rendering and the values before and after it changed.
import { useState, useEffect, useRef } from 'react';// Let's pretend that the re-rendering cost of this <Counter> component is very high...//We wrapped it up with React.memo, but we still need to find performance issues: ///So we added useWhyDidYouUpdate and looked at the console to see what constCounter = React.memo (props =>) would happen.{ useWhyDidYouUpdate('Counter', props); return <div style={props.style}>{props.count}</div>;});function App() { const [count, setCount] = useState(0); const [userId, setUserId] = useState(0); // Our console tells us about the < Counter > style prop... // ... changes in every rendering, even if we only change the status of userId by pushing a button... // ... That's because counterStyle is recreated every time it's re-rendered. // Thanks to our hook, we found this problem and reminded us that maybe we should move this object outside of component. const counterStyle = { fontSize: '3rem', color: 'red' }; return ( <div> <div className="counter"> <Counter count={count} style={counterStyle} /> <button onClick={() => setCount(count + 1)}>Increment</button> </div> <div className="user"> <img src={`http://i.pravatar.cc/80?img=${userId}`} /> <button onClick={() => setUserId(userId + 1)}>Switch User</button> </div> </div> );}// Hookfunction useWhyDidYouUpdate(name, props) { // Get a variable kef object that we can use to store props and compare them when the next hook runs const previousProps = useRef(); useEffect(() => { if (previousProps.current) { // Get the key value of all props before and after the change const allKeys = Object.keys({ ...previousProps.current, ...props }); // Use this object to track changes in props const changesObj = {}; // Loop through the key value allKeys.forEach(key => { // Determine whether the value before the change is consistent with the current one if (previousProps.current[key] !== props[key]) { // Add prop to the object to be tracked changesObj[key] = { from: previousProps.current[key], to: props[key] }; } }); // If the changed props are not empty, they are output to the console if (Object.keys(changesObj).length) { console.log('[why-did-you-update]', name, changesObj); } } // Finally, save the current props value in previousProps for use in the next hook previousProps.current = props; });}
useDarkMode
This hook contains all the state logic when you need to add a dark mode to your website. It uses local Storage to remember user-selected patterns, default browsers, or system-level settings, and uses prefers-color-schema media queries and management. dark-mode class names to apply your own style on the body. This article can also help you understand the power of combining hooks. The user Local Storage hook is used to synchronize the state in the state to the local Storage. Use Meidahook to detect user preferences for dark mode. Both hooks were created in other cases, but here we combine them to create a very useful hook with a fairly small number of rows. It's almost as if hooks bring the composite power of React components to stateful logic! (12)
// Usagefunction App() { const [darkMode, setDarkMode] = useDarkMode(); return ( <div> <div className="navbar"> <Toggle darkMode={darkMode} setDarkMode={setDarkMode} /> </div> <Content /> </div> );}// Hookfunction useDarkMode() { // Use our useLocalStorage hook to save state even after page refresh const [enabledState, setEnabledState] = useLocalStorage('dark-mode-enabled'); // See if the user has set a browser or system preference for dark mode // UsePrefers DarkMode hook combines useMedia hook (see the next code) const prefersDarkMode = usePrefersDarkMode(); // If enabledState is defined use it, otherwise fallback to prefersDarkMode. // This allows users to override system-level settings on our website const enabled = typeof enabledState !== 'undefined' ? enabledState : prefersDarkMode; // Change the Dark Patterns useEffect( () => { const className = 'dark-mode'; const element = window.document.body; if (enabled) { element.classList.add(className); } else { element.classList.remove(className); } }, [enabled] // Just call this method when enabled changes ); // Return enabled status and setup method return [enabled, setEnabledState];}// Combining useMedia hook to detect dark mode preferences // useMedia is designed to support multiple media queries and return values. // Thanks to the combination of hooks, we can hide the complexity of this piece // useMedia's method in the next article, function usePrefers DarkMode (){ return useMedia(['(prefers-color-scheme: dark)'], [true], false);}
donavon/use-dark-mode - This hook is a more configurable implementation, and synchronizes tab and SSR situations in different browsers. This article provides a lot of code and inspiration.
useMedia
This hook allows you to easily use media queries in your component logic. In our example, we can match the width of the current screen and render different columns according to which media query. Then we assign pictures to different positions in the column to limit the height difference of the column (we don't want one column to be longer than the rest). Instead of using media queries, you can create a hook that captures the screen width directly. But this approach will make it easier for you to share media queries with JS and your Stylesheet.
import { useState, useEffect } from 'react';function App() { const columnCount = useMedia( // Media Inquiry ['(min-width: 1500px)', '(min-width: 1000px)', '(min-width: 600px)'], // Number of columns (associated with the media query array above by subscript) [5, 4, 3], // Default column number 2 ); // Create a default column height array filled with 0 let columnHeights = new Array(columnCount).fill(0); // Create an array to store the elements of each column, each item of the array being an array let columns = new Array(columnCount).fill().map(() => []); data.forEach(item => { // Get the shortest item const shortColumnIndex = columnHeights.indexOf(Math.min(...columnHeights)); // Add item columns[shortColumnIndex].push(item); // Update height columnHeights[shortColumnIndex] += item.height; }); // Render each column and its elements return ( <div className="App"> <div className="columns is-mobile"> {columns.map(column => ( <div className="column"> {column.map(item => ( <div className="image-container" style={{ // Adjust the picture container according to the length-width ratio of the picture paddingTop: (item.height / item.width) * 100 + '%' }} > <img src={item.image} alt="" /> </div> ))} </div> ))} </div> </div> );}// Hookfunction useMedia(queries, values, defaultValue) { // An array containing whether to match each media query const mediaQueryLists = queries.map(q => window.matchMedia(q)); // Method of Value Selection Based on Matched Media Query const getValue = () => { // Get the subscript of the first matching media query const index = mediaQueryLists.findIndex(mql => mql.matches); // Returns the corresponding value or default value return typeof values[index] !== 'undefined' ? values[index] : defaultValue; }; // state and setter of matched values const [value, setValue] = useState(getValue); useEffect( () => { // callback // Note: By defining getValue outside useEffect... // ... We can determine the latest value that it passes in from the hook's parameters (the hook's callback was first created on mount) const handler = () => setValue(getValue); // Set up a listener as a callback for each of the above media queries mediaQueryLists.forEach(mql => mql.addListener(handler)); // Clearing up listeners in cleanup return () => mediaQueryLists.forEach(mql => mql.removeListener(handler)); }, [] // An empty array guarantees that effect runs only on mount and unmount ); return value;}
useMedia v1 - The original solution of this small method, which uses an event to listen for browser resize events, works well, but is only useful for screen width media queries. Masonry Grid - useMedia v1 source code. This demo uses react-spring to animate when the image changes the number of columns.
useLockBodyScroll
Sometimes when special components are displayed on your pages, you want to prevent users from sliding your pages (think of modal boxes or full-screen menus on the mobile side). This can be confusing if you see content scrolling under the modal box, especially when you plan to scroll through the modal box. This hook solves this problem. Using this hook in any component, the page will only be unlocked and slid if, of course, the component is unmount ed.
import { useState, useLayoutEffect } from 'react';// Use function App(){ // state of modal box const [modalOpen, setModalOpen] = useState(false); return ( <div> <button onClick={() => setModalOpen(true)}>Show Modal</button> <Content /> {modalOpen && ( <Modal title="Try scrolling" content="I bet you you can't! Muahahaha đ" onClose={() => setModalOpen(false)} /> )} </div> );}function Modal({ title, content, onClose }){ // Call hook to lock body scroll useLockBodyScroll(); return ( <div className="modal-overlay" onClick={onClose}> <div className="modal"> <h2>{title}</h2> <p>{content}</p> </div> </div> );}// Hookfunction useLockBodyScroll() { useLayoutEffect(() => { // Get the overflow value of the original body const originalStyle = window.getComputedStyle(document.body).overflow; //Prevent scrolling during mount document.body.style.overflow = 'hidden'; // Unlock scroll when component unmount return () => document.body.style.overflow = originalStyle; }, []); // An empty array guarantees that the effect function will only run on mount and unmount}
How hooks may shape desgin systems built in React - A great article that inspired this little method. Their version of useLockBodyScroll hook accepts a switch parameter to provide more control over the locking state.
useTheme
This hook helps you to use CSS variables to dynamically change the performance of your app. You simply pass a CSS variable in the root element of your document that you want to update and hook each variable that contains the key-value pair. This is useful when you can't use in-line styles (without pseudo-Class support) and there are too many ways to arrange your theme styles (such as an app application that allows users to customize their appearance). It's worth noting that many css-in-js libraries support dynamic styles, but it's interesting to try using only CSS variables and a React hook. The following example is very simple, but you can imagine that the subject object is stored in the state or retrieved from the interface. Be sure to look at this interesting online example.
import { useLayoutEffect } from 'react';import './styles.scss'; // -> https://codesandbox.io/s/15mko9187// Usageconst theme = { 'button-padding': '16px', 'button-font-size': '14px', 'button-border-radius': '4px', 'button-border': 'none', 'button-color': '#FFF', 'button-background': '#6772e5', 'button-hover-border': 'none', 'button-hover-color': '#FFF'};function App() { useTheme(theme); return ( <div> <button className="button">Button</button> </div> );}// Hookfunction useTheme(theme) { useLayoutEffect( () => { // Loop this topic object for (const key in theme) { // Update the css variable of the document root element document.documentElement.style.setProperty(`--${key}`, theme[key]); } }, [theme] // It will only run again if the subject object publication changes );}
CSS Variables and React - A blog post inspiring this little method, from Dan Bahrami.
useSpring
This hook is part of react-spring, a library that lets you use high-performance physical animation. I'm trying to avoid introducing dependencies here, but this time I'll make an exception in order to expose this very useful library. One of the advantages of react-spring is that it allows you to completely skip the life cycle of React render when you use animation. This often leads to objective performance improvements. In the next example, we will render a row of cards and apply spring animation based on the position of the mouse over each card. To achieve this effect, we call useSpring hook using an array of values to be transformed. Render an animation component (exported by react-spring) and use the onMouseMove event to get the mouse position. Then call setAnimationProps (the function returned by hook) to update. You can read the comments of the code below or view the online examples directly.
import { useState, useRef } from 'react';import { useSpring, animated } from 'react-spring';// Display a row of cards // Usage of hooks is within <Card> component belowfunction App(){ return ( <div className="container"> <div className="row"> {cards.map((card, i) => ( <div className="column"> <Card> <div className="card-title">{card.title}</div> <div className="card-body">{card.description}</div> <img className="card-image" src={card.image} /> </Card> </div> ))} </div> </div> );}function Card({ children }) { // We use this ref to store the offset values and sizes of elements retrieved from onMouseMove events const ref = useRef(); // Keep track of whether the card is hover, so that we can ensure that the level of the card is on top of other animations. const [isHovered, setHovered] = useState(false); // The useSpring hook const [animatedProps, setAnimatedProps] = useSpring({ // An array to store these values [rotateX, rotateY, and scale] // We use a combined key (xys) instead of separate keys, so that we can update the value of css transform using animated Props. xys. interpolate (). xys: [0, 0, 1], // Setup physics config: { mass: 10, tension: 400, friction: 40, precision: 0.00001 } }); return ( <animated.div ref={ref} className="card" onMouseEnter={() => setHovered(true)} onMouseMove={({ clientX, clientY }) => { // Get the position of the mouse X coordinate relative to the card const x = clientX - (ref.current.offsetLeft - (window.scrollX || window.pageXOffset || document.body.scrollLeft)); // Get the position of the mouse Y relative to the card const y = clientY - (ref.current.offsetTop - (window.scrollY || window.pageYOffset || document.body.scrollTop)); // Set the value of the animation according to the position of the mouse and the size of the card const dampen = 50; // The smaller the number, the smaller the rotation angle. const xys = [ -(y - ref.current.clientHeight / 2) / dampen, // rotateX (x - ref.current.clientWidth / 2) / dampen, // rotateY 1.07 // Scale ]; // Update the value of the animation setAnimatedProps({ xys: xys }); }} onMouseLeave={() => { setHovered(false); // Reducing the value of xys setAnimatedProps({ xys: [0, 0, 1] }); }} style={{ // When a card is hover ed, we want it to be hierarchical over other cards. zIndex: isHovered ? 2 : 1, // Functions for handling css changes transform: animatedProps.xys.interpolate( (x, y, s) => `perspective(600px) rotateX(${x}deg) rotateY(${y}deg) scale(${s})` ) }} > {children} </animated.div> );}
useHistory
This hook can easily add undo/redo functionality to your application. Our case is a simple painting application. This example will generate a grid block that you can click on to change its color, and by using the useHistory hook, we can undo, redo, or clear all changes on canvas. Online examples. In our hook, we will use user Roducer instead of useState to store data, which should be familiar to anyone who has used redux (see the official documentation for more information on useReducer). This hook replicates the use-undo library with some minor changes. So you can install and use the library directly through npm.
import { useReducer, useCallback } from 'react';// Usagefunction App() { const { state, set, undo, redo, clear, canUndo, canRedo } = useHistory({}); return ( <div className="container"> <div className="controls"> <div className="title">đŠâđ¨ Click squares to draw</div> <button onClick={undo} disabled={!canUndo}> Undo </button> <button onClick={redo} disabled={!canRedo}> Redo </button> <button onClick={clear}>Clear</button> </div> <div className="grid"> {((blocks, i, len) => { // Generate a grid block while (++i <= len) { const index = i; blocks.push( <div // If the state in state is true, add the active class name to the block className={'block' + (state[index] ? ' active' : '')} // Change the status of the block by clicking and merge it into the latest state onClick={() => set({ ...state, [index]: !state[index] })} key={i} /> ); } return blocks; })([], 0, 625)} </div> </div> );}// Initialize stateconst initial state in useReducer={ // Each time we add a new state, we store an array of pre-update States past: [], // Current state value present: null, // Let's use a future array with redo functionality future: []};// Change const reducer = (state, action) => to process state according to action{ const { past, present, future } = state; switch (action.type) { case 'UNDO': const previous = past[past.length - 1]; const newPast = past.slice(0, past.length - 1); return { past: newPast, present: previous, future: [present, ...future] }; case 'REDO': const next = future[0]; const newFuture = future.slice(1); return { past: [...past, present], present: next, future: newFuture }; case 'SET': const { newPresent } = action; if (newPresent === present) { return state; } return { past: [...past, present], present: newPresent, future: [] }; case 'CLEAR': const { initialPresent } = action; return { ...initialState, present: initialPresent }; }};// Hookconst useHistory = initialPresent => { const [state, dispatch] = useReducer(reducer, { ...initialState, present: initialPresent }); const canUndo = state.past.length !== 0; const canRedo = state.future.length !== 0; // Setting up our callback function // Use useCallback to avoid unnecessary re-rendering const undo = useCallback( () => { if (canUndo) { dispatch({ type: 'UNDO' }); } }, [canUndo, dispatch] ); const redo = useCallback( () => { if (canRedo) { dispatch({ type: 'REDO' }); } }, [canRedo, dispatch] ); const set = useCallback(newPresent => dispatch({ type: 'SET', newPresent }), [ dispatch ]); const clear = useCallback(() => dispatch({ type: 'CLEAR', initialPresent }), [ dispatch ]); // If necessary, it can also be found everywhere in the past and in the future. return { state: state.present, set, undo, redo, clear, canUndo, canRedo };};
xxhomey19/use-undo - The libraries mentioned above also return the status of previous and future from hook, but there is no clear action React useHistory hook - another way to implement useHistory hook.
useScript
Using this hook allows you to simply dynamically load the ipt of the external scr and know when it's loaded. This hook is useful when you need to rely on a third-party library and want to load it on demand instead of requesting it at the head of each page. In the following example, we won't call the method we declared in script until the script is loaded. If you are interested in learning how to implement this advanced component, you can take a look at the source of react-script-loader-hoc. Personally, I think it's more readable than this hook. Another advantage is that it is easier to call a hook to load multiple different scripts than the implementation of this higher-order component, which is supported by adding multiple src strings.
import { useState, useEffect } from 'react';// Usagefunction App() { const [loaded, error] = useScript( 'https://pm28k14qlj.codesandbox.io/test-external-script.js' ); return ( <div> <div> Script loaded: <b>{loaded.toString()}</b> </div> {loaded && !error && ( <div> Script function call response: <b>{TEST_SCRIPT.start()}</b> </div> )} </div> );}// Hooklet cachedScripts = [];function useScript(src) { // Continuous tracking of script load completion and failure status const [state, setState] = useState({ loaded: false, error: false }); useEffect( () => { // If this src exists in the cachedScripts array, it loads the script on behalf of another hook instance, so there is no need to load it again. if (cachedScripts.includes(src)) { setState({ loaded: true, error: false }); } else { cachedScripts.push(src); // Create script tags let script = document.createElement('script'); script.src = src; script.async = true; // Script Event Monitoring Method const onScriptLoad = () => { setState({ loaded: true, error: false }); }; const onScriptError = () => { // When it fails, remove cachedScripts so that we can try loading again const index = cachedScripts.indexOf(src); if (index >= 0) cachedScripts.splice(index, 1); script.remove(); setState({ loaded: true, error: true }); }; script.addEventListener('load', onScriptLoad); script.addEventListener('error', onScriptError); // Add script to the document document.body.appendChild(script); // Clear event listeners in cleanup callbacks return () => { script.removeEventListener('load', onScriptLoad); script.removeEventListener('error', onScriptError); }; } }, [src] // Rerun only when the src changes ); return [state.loaded, state.error];}
The react-script-loader-hoc implementation of the same logic can be used for comparison. useScript from palmerhq/the-platform - similar hook, but using React Suspense to return a promise
useKeyPress
Using this hook, you can easily monitor when users enter special keys on their keyboard. This tip is very simple, and I want to show you that it only takes a little bit of code, but I challenge any reader to see who can create a more advanced version. Monitoring is a good complement when multiple keys are pressed at the same time. Subentry: It also detects whether key values are entered in the specified order.
const happyPress = useKeyPress('h'); const sadPress = useKeyPress('s'); const robotPress = useKeyPress('r'); const foxPress = useKeyPress('f'); return ( <div> <div>h, s, r, f</div> <div> {happyPress && 'đ'} {sadPress && 'đĸ'} {robotPress && 'đ¤'} {foxPress && 'đĻ'} </div> </div> );}// Hookfunction useKeyPress(targetKey) { // Used to store continuous tracking if keys are pressed const [keyPressed, setKeyPressed] = useState(false); // If the pressed key is our target value, set it to true function downHandler({ key }) { if (key === targetKey) { setKeyPressed(true); } } // If the loosened key is our target value, set it to false const upHandler = ({ key }) => { if (key === targetKey) { setKeyPressed(false); } }; // Adding event listeners useEffect(() => { window.addEventListener('keydown', downHandler); window.addEventListener('keyup', upHandler); // Clear callbacks in cleanup return () => { window.removeEventListener('keydown', downHandler); window.removeEventListener('keyup', upHandler); }; }, []); // Empty arrays mean that only mount and unmout run return keyPressed;}
useMultiKeyPress - This example detects multiple key values simultaneously.
useMemo
React has a built-in hook called useMemo, which allows you to cache expensive methods to avoid them being called in every render. You can simply pass in functions and arrays and useMemo will recalculate only if one of the inputs changes. Here's a cost-effective function called computeLetterCount in our example (for demonstration purposes, we reduce speed by including a completely unnecessary loop). When the currently selected word changes, you will observe the delay caused by the need to call the computeLetterCount method again for the new word. We also have a counter to increase the count every time the button is clicked. When the counter increases, you will find that there is no delay before two renders. This is because computeLetterCount is not called. The input text does not change, so the cached value is returned.
import { useState, useMemo } from 'react';// Usagefunction App() { // state of Counter const [count, setCount] = useState(0); // Track the current words we want to show in the array const [wordIndex, setWordIndex] = useState(0); // We can browse words and see the number of letters. const words = ['hey', 'this', 'is', 'cool']; const word = words[wordIndex]; // Returns the number of letters in a word // Artificially slow it down const computeLetterCount = word => { let i = 0; while (i < 1000000000) i++; return word.length; }; // Cache computeLetterCount, which returns the cached value when the value of the input array is the same as the last run const letterCount = useMemo(() => computeLetterCount(word), [word]); // This method will cause us to increase the count and become delayed because we have to wait for expensive methods to run again. //const letterCount = computeLetterCount(word); return ( <div style={{ padding: '15px' }}> <h2>Compute number of letters (slow đ)</h2> <p>"{word}" has {letterCount} letters</p> <button onClick={() => { const next = wordIndex + 1 === words.length ? 0 : wordIndex + 1; setWordIndex(next); }} > Next word </button> <h2>Increment a counter (fast âĄī¸)</h2> <p>Counter: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> );}
useDebounce
This hook allows for jittering of any rapidly changing value. The value of de-jitter changes only if the latest value is not invoked within a specified time interval. For example, in the following example, we use it in conjunction with useEffect, and you can easily ensure that expensive operations like API calls are not frequently invoked. In the following example, we will use the Mandarin Comic API to search, and use Debounce to prevent the API from being called every time a key is pressed, resulting in you being blocked by the interface.
import { useState, useEffect, useRef } from 'react';// Usagefunction App() { // Search terms const [searchTerm, setSearchTerm] = useState(''); // API search results const [results, setResults] = useState([]); // Search status (whether there are pending requests) const [isSearching, setIsSearching] = useState(false); // To change the search word jitter, only when the search word has not changed in 500 milliseconds will the latest value be returned. // The goal is to invoke the API only when the user stops typing, to prevent us from invoking the API too quickly and frequently. const debouncedSearchTerm = useDebounce(searchTerm, 500); // Effect for API call useEffect( () => { if (debouncedSearchTerm) { setIsSearching(true); searchCharacters(debouncedSearchTerm).then(results => { setIsSearching(false); setResults(results); }); } else { setResults([]); } }, [debouncedSearchTerm] // Called only when the search term changes after jitter removal ); return ( <div> <input placeholder="Search Marvel Comics" onChange={e => setSearchTerm(e.target.value)} /> {isSearching && <div>Searching ...</div>} {results.map(result => ( <div key={result.id}> <h4>{result.title}</h4> <img src={`${result.thumbnail.path}/portrait_incredible.${ result.thumbnail.extension }`} /> </div> ))} </div> );}// API search functionfunction searchCharacters(search) { const apiKey = 'f9dfb1e8d466d36c27850bedd2047687'; return fetch( `https://gateway.marvel.com/v1/public/comics?apikey=${apiKey}&titleStartsWith=${search}`, { method: 'GET' } ) .then(r => r.json()) .then(r => r.data.results) .catch(error => { console.error(error); return []; });}// Hookfunction useDebounce(value, delay) { // Store de-jitter values const [debouncedValue, setDebouncedValue] = useState(value); useEffect( () => { // Update the de-jitter value after delay ing const handler = setTimeout(() => { setDebouncedValue(value); }, delay); // Cancel timeout if the value changes (also in effect when delay changes or unmount) // This is how we can prevent the value from jittering, empty the timeout and rerun it by keeping the internal value of the delay interval unchanged. return () => { clearTimeout(handler); }; }, [value, delay] // Re-invocation occurs only when the search value or the delay value changes ); return debouncedValue;}
useOnScreen
This hook allows you to easily detect whether an element is visible on the screen and specify how many elements should be displayed on the screen. When the user scrolls to a specific area, it is very suitable for lazy loading of pictures or triggering animation.
import { useState, useEffect, useRef } from 'react';// Usagefunction App() { // Used to store elements that we want to detect on the screen const ref = useRef(); // Call hook and pass in ref and root margin // In this case, only elements with more than 300 PX will be displayed on the screen. const onScreen = useOnScreen(ref, '-300px'); return ( <div> <div style={{ height: '100vh' }}> <h1>Scroll down to next section đ</h1> </div> <div ref={ref} style={{ height: '100vh', backgroundColor: onScreen ? '#23cebd' : '#efefef' }} > {onScreen ? ( <div> <h1>Hey I'm on the screen</h1> <img src="https://i.giphy.com/media/ASd0Ukj0y3qMM/giphy.gif" /> </div> ) : ( <h1>Scroll down 300px from the top of this section đ</h1> )} </div> </div> ); } // Hook function useOnScreen(ref, rootMargin = '0px') { // The state of storage elements visible const [isIntersecting, setIntersecting] = useState(false); useEffect(() => { const observer = new IntersectionObserver( ([entry]) => { // When the observer callback triggers the update status setIntersecting(entry.isIntersecting); }, { rootMargin } ); if (ref.current) { observer.observe(ref.current); } return () => { observer.unobserve(ref.current); }; }, []); // Empty arrays ensure that only mount and unmount are executed return isIntersecting; }
react-intersection-observer-a more robust and configurable implementation.
usePrevious
A common problem is how to get the values before props and state when using hooks. In React's class component, we have the componentDidUpdate method to receive the previous props and state in the form of parameters, or your client updates an instance variable (this.previous = value) and references it later to get the previous value. So how can we do this in functional components without lifecycle methods or instance stored values? Hook to fight the fire. We can create a custom hook that uses useRef hook to store the value before it is internally stored. See the following examples and in-line comments. Or look directly at official examples
import { useState, useEffect, useRef } from 'react';// Usagefunction App() { const [count, setCount] = useState(0); // Get the value before the update (pass hook in the last render) const prevCount = usePrevious(count); // Display both current and pre-update values return ( <div> <h1>Now: {count}, before: {prevCount}</h1> <button onClick={() => setCount(count + 1)}>Increment</button> </div> );}// Hookfunction usePrevious(value) { // ref object is a general container whose current attribute is variable and can hold any value, similar to instance attribute on a class. const ref = useRef(); // Store current value in ref useEffect(() => { ref.current = value; }, [value]); // Rerun only when the value changes // Returns the value before the update (before the useEffect update) return ref.current;}
useOnClickOutside
This hook allows you to monitor whether to click outside a particular element. In the next example, we use it to close the modal box when any element outside the modal box is clicked. By abstracting this logic into a hook, we can easily use it in components that require similar functionality (drop-down menus, prompts, etc.).
import { useState, useEffect, useRef } from 'react';// Usagefunction App() { // Create a ref that stores the elements we want to monitor for external clicks const ref = useRef(); // Logic of modal box const [isModalOpen, setModalOpen] = useState(false); // Call hook and pass in the function to trigger when ref and external 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 => { // Click inside the element to do nothing 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); }; }, // Add ref and processing functions to the dependency array of effect // It's worth noting that because the processing method passed in each render is a new function, this will cause the effect callback and cleanup to be called one at each render. // It's not a big problem either. You can wrap the handler through useCallback and then pass it into hook. [ref, handler] ); }
[Andarist/use-onclickoutside] - Logical-like libraries. If you want to pull something from github/npm, this library is a good choice.
useAnimation
This hook allows you to smooth any animated value (linear elastic) through a slowdown function. In the example, we call use Animation hook three times to let three different balls complete the animation at different intervals. As an added point, we also show how to combine hooks is very simple. Instead of actually using useState or useEffect itself, our useAnimation hook wraps it up using useAnimation Timer hook. Extracting timer-related logic from hooks makes our code more readable and can use timer logic in other links.
import { useState, useEffect } from 'react';// Usagefunction App() { // Call hook s multiple times at different startup delays to get different animation values 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) { // UseAnimation Timer calls useState in every frame of our given time to make the animation as smooth as possible. const elapsed = useAnimationTimer(duration, delay); // Total amount of specified duration in a time range of 0-1 const n = Math.min(1, elapsed / duration); // Returns the modified value according to the slowdown function we specified return easing[easingName](n);}// Address of some slowdown functions: // https://github.com/streamich/ts-easing/blob/master/src/index.ts// Hardcoded here or introduced 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; // Functions to be executed in each frame of animation function onFrame() { setTime(Date.now() - start); loop(); } // Call onFrame() on the next frame function loop() { animationFrame = requestAnimationFrame(onFrame); } function onStart() { // Set a timeout to stop when the duration exceeds timerStop = setTimeout(() => { cancelAnimationFrame(animationFrame); setTime(Date.now() - start); }, duration); // Start the cycle start = Date.now(); loop(); } // Execute after the specified delay (defaults to 0) const timerDelay = setTimeout(onStart, delay); // Clean things up return () => { clearTimeout(timerStop); clearTimeout(timerDelay); cancelAnimationFrame(animationFrame); }; }, [duration, delay] // Rerun only when duration and delay change ); return elapsed;}
useWindowSize
A really common requirement is to get the size of the browser's current window. This hook returns the object that contains the width and height. If executed on the server side (without a window object), the width and height values are undefined.
import { useState, useEffect } from 'react';// Usagefunction App() { const size = useWindowSize(); return ( <div> {size.width}px / {size.height}px </div> );}// Hookfunction useWindowSize() { const isClient = typeof window === 'object'; function getSize() { return { width: isClient ? window.innerWidth : undefined, height: isClient ? window.innerHeight : undefined }; } const [windowSize, setWindowSize] = useState(getSize); useEffect(() => { if (!isClient) { return false; } function handleResize() { setWindowSize(getSize()); } window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); // An empty array guarantees that effect will only be performed on mount and unmount return windowSize;}
useHover
Monitor whether a mouse moves to an element. This hook returns a ref and a Boolean value, which indicates whether the element currently having the ref is hover. So just add the returned ref to any element you want to listen for the hover status.
import { useRef, useState, useEffect } from 'react';// Usagefunction App() { const [hoverRef, isHovered] = useHover(); return ( <div ref={hoverRef}> {isHovered ? 'đ' : 'âšī¸'} </div> );}// Hookfunction 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] // Only when ref changes will it be called again ); return [ref, value];}
useLocalStorage
Synchronize the data in the state to local storage so that the state can be saved when the page refreshes. Similar to useState, we just pass in a localstorage value so that it is used by default when the page is loaded, rather than the specified initial value.
import { useState } from 'react';// Usagefunction App() { // Similar to useState, but the first parameter is the key value in the localstorage const [name, setName] = useLocalStorage('name', 'Bob'); return ( <div> <input type="text" placeholder="Enter your name" value={name} onChange={e => setName(e.target.value)} /> </div> );}// Hookfunction useLocalStorage(key, initialValue) { // State to store our value // Pass the initial state to useState so that the logic executes only once const [storedValue, setStoredValue] = useState(() => { try { // Get values from localstorage by key values const item = window.localStorage.getItem(key); // Resolve stored json if no initial value is returned return item ? JSON.parse(item) : initialValue; } catch (error) { // Return the initial value if the error is reported console.log(error); return initialValue; } }); // Returns the wrapped version of useState's setter function, which saves the new value to the localstorage const setValue = value => { try { // The allowable value is a function, so we have the same api as useState const valueToStore = value instanceof Function ? value(storedValue) : value; // Save state setStoredValue(valueToStore); // Save to local Storage window.localStorage.setItem(key, JSON.stringify(valueToStore)); } catch (error) { // Processing of higher-level implementations will handle errors console.log(error); } }; return [storedValue, setValue];}
use-persisted-state - A more advanced implementation that can be synchronized between different tab s and browser windows.