React: useHooks tips

Keywords: React Spring JSON Mobile

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.

Posted by sspoke on Fri, 02 Aug 2019 01:29:22 -0700