In this series, I will read the source code of ahooks, which is used to familiarized myself with the writing method of custom hooks, and improve my ability to write custom hooks.

In order to distinguish it from the original comments of the code, the personal understanding section uses the beginning of ///, which is not related to the triple slash instruction, just to distinguish it.

Review past

  • Ahooks source code interpretation series
  • Ahooks Source Code Interpretation Series – 2
  • Ahooks Source Code Interpretation series – 3
  • Ahooks Source Code Interpretation series – 4
  • Ahooks Source Code Interpretation series – 5
  • Ahooks Source Code Interpretation series – 6
  • Ahooks Source Code Interpretation series – 7
  • Ahooks Source Code Interpretation series – 8
  • Ahooks Source Code Interpretation series – 9
  • Ahooks Source Code Interpretation series – 10
  • Ahooks Source Code Interpretation series – 11
  • Ahooks Source Code Interpretation series – 12

Happy Tuesday is coming, everyone should be more energetic, today will watch the rest of the Dom part hook, thank you for coming to read 🙏 ~

useTextSelection

Dash words may be used in translation

/ / /...

const initRect: IRect = {
  top: NaN.left: NaN.bottom: NaN.right: NaN.height: NaN.width: NaN};const initState: IState = {
  text: ' '. initRect, };function getRectFromSelection(selection: Selection | null) :IRect {
  if(! selection) {return initRect;
  }

  if (selection.rangeCount < 1) {
    return initRect;
  }
  const range = selection.getRangeAt(0);
  const { height, width, top, left, right, bottom } = range.getBoundingClientRect();
  return {
    height,
    width,
    top,
    left,
    right,
    bottom,
  };
}

/** * Gets the text selected by the user or the current cursor insertion position ** /
function useTextSelection(target? : BasicTarget) :IState {
  const [state, setState] = useState(initState);

  const stateRef = useRef(state);
  stateRef.current = state;

  useEffect(() = > {
    // Get the target in useEffect, otherwise the component will not load properly and the element will not get
    const el = getTargetElement(target, document);

    if(! el) {return () = > {};
    }

    const mouseupHandler = () = > {
      let selObj: Selection | null = null;
      let text = ' ';
      let rect = initRect;
      if (!window.getSelection) return;
      selObj = window.getSelection();
      text = selObj ? selObj.toString() : ' ';
      if (text) { // Set the latest selection data information after selecting the contentrect = getRectFromSelection(selObj); setState({ ... state, text, ... rect }); }};// Any click needs to clear the previous range
    const mousedownHandler = () = > {
      /// window.getSelection is a precondition for the hook to work, and can be placed first so that useless events are not bound
      if (!window.getSelection) return;
      if(stateRef.current.text) { setState({ ... initState }); }const selObj = window.getSelection();
      if(! selObj)return;
      selObj.removeAllRanges();
    };

    el.addEventListener('mouseup', mouseupHandler);

    document.addEventListener('mousedown', mousedownHandler);

    return () = > {
      el.removeEventListener('mouseup', mouseupHandler);
      document.removeEventListener('mousedown', mousedownHandler); }; },typeof target === 'function' ? undefined : target]);

  return state;
}

export default useTextSelection;

Copy the code

UseHover, useMouse, useDocumentVisibility, useInViewport, useResponsive

Some DOM status identification, DOM data acquisition hooks

Update hover status value in real time by listening to mouseEnter and Mouseleve events

/ / /...

export default(target: BasicTarget, options? : Options):boolean= > {
  const { onEnter, onLeave } = options || {};

  const [state, { setTrue, setFalse }] = useBoolean(false);

  useEventListener(
    'mouseenter'.() = > {
      onEnter && onEnter();
      setTrue();
    },
    {
      target,
    },
  );

  useEventListener(
    'mouseleave'.() = > {
      onLeave && onLeave();
      setFalse();
    },
    {
      target,
    },
  );

  return state;
};
Copy the code

Update mouse coordinate data in real time by listening to Mousemove events

/ / /...

export default() = > {const [state, setState] = useState(initState);

  useEventListener(
    'mousemove'.(event: MouseEvent) = > {
      const { screenX, screenY, clientX, clientY, pageX, pageY } = event;
      setState({ screenX, screenY, clientX, clientY, pageX, pageY });
    },
    {
      target: document,});return state;
};
Copy the code

Update document visible status values in real time by listening to visiBilityChange events

export default function canUseDom() {
  return!!!!! (typeof window! = ='undefined' && window.document && window.document.createElement);
}

/ / /...

const getVisibility = () = > {
  if(! canUseDom())return 'visible';
  return document.visibilityState;
};

function useDocumentVisibility() :VisibilityState {
  const [documentVisibility, setDocumentVisibility] = useState(() = > getVisibility());

  useEventListener(
    'visibilitychange'.() = > {
      setDocumentVisibility(getVisibility());
    },
    {
      target: () = > document./// use document instead of method});return documentVisibility;
}

export default useDocumentVisibility;

Copy the code

Through the IntersectionObserver API, the status of IntersectionObserver can be updated in real time

import { useEffect, useState } from 'react';
import 'intersection-observer';
import { getTargetElement, BasicTarget } from '.. /utils/dom';

type InViewport = boolean | undefined;

function isInViewPort(el: HTMLElement) :InViewport  {
  if(! el) {return undefined;
  }

  const viewPortWidth =
    window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
  const viewPortHeight =
    window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
  const rect = el.getBoundingClientRect();
  // if either side is completely out of the viewport, it is considered not in the viewport
  if (rect) {
    const { top, bottom, left, right } = rect;
    return bottom > 0 && top <= viewPortHeight && left <= viewPortWidth && right > 0;
  }
  / / / the rect didn't may be empty, so there is no need for the rect to https://developer.mozilla.org/zh-CN/docs/Web/API/Element/getBoundingClientRect
  return false;
}

function useInViewport(target: BasicTarget) :InViewport {
  const [inViewPort, setInViewport] = useState<InViewport>(() = > {
    const el = getTargetElement(target);

    return isInViewPort(el as HTMLElement);
  });

  useEffect(() = > {
    const el = getTargetElement(target);
    if(! el) {return () = > {};
    }
    // Use IntersectionObserver to monitor the intersecting status of target and viewport so as to update the identification of whether it is at the viewport
    const observer = new IntersectionObserver((entries) = > {
      for (const entry of entries) {
        if (entry.isIntersecting) {
          setInViewport(true);
        } else {
          setInViewport(false); }}}); observer.observe(elas HTMLElement);

    return () = > {
      observer.disconnect();
    };
  }, [target]);

  return inViewPort;
}

export default useInViewport;

Copy the code

Update the corresponding data in real time by listening to the window resize event

import { useEffect, useState } from 'react';

type Subscriber = () = > void;

const subscribers = new Set<Subscriber>();

interface ResponsiveConfig {
  [key: string] :number;
}
interface ResponsiveInfo {
  [key: string] :boolean;
}

let info: ResponsiveInfo;
/// Default breakpoint data
let responsiveConfig: ResponsiveConfig = {
  xs: 0.sm: 576.md: 768.lg: 992.xl: 1200};function handleResize() {
  const oldInfo = info;
  calculate();
  if (oldInfo === info) return;
  for (const subscriber ofsubscribers) { subscriber(); }}let listening = false;

function calculate() {
  const width = window.innerWidth;
  const newInfo = {} as ResponsiveInfo;
  let shouldUpdate = false;
  for (const key of Object.keys(responsiveConfig)) {
    newInfo[key] = width >= responsiveConfig[key];
    if(newInfo[key] ! == info[key]) { shouldUpdate =true; }}if(shouldUpdate) { info = newInfo; }}export function configResponsive(config: ResponsiveConfig) {
  responsiveConfig = config;
  if (info) calculate();
}
Subscribers and Listening are employed to reduce window event binding
export function useResponsive() {
  const windowExists = typeof window! = ='undefined';
  /// Use the listening global flag to prevent multiple useResponsive registration events
  if(windowExists && ! listening) { info = {}; calculate();window.addEventListener('resize', handleResize);
    listening = true;
  }
  const [state, setState] = useState<ResponsiveInfo>(info);

  useEffect(() = > {
    if(! windowExists)return;

    const subscriber = () = > {
      setState(info);
    };
    Subscribers collect state change operations of every useResponsive in subscribers
    subscribers.add(subscriber);
    return () = > {
      subscribers.delete(subscriber);
      // if Subscribers are already subscribers, all useResponsive is already uninstalled and events can be removed
      if (subscribers.size === 0) {
        window.removeEventListener('resize', handleResize);
        listening = false; }}; } []);return state;
}

Copy the code

useExternal

“One-click skin change to get to know”

/ / /...

export default function useExternal(path: string, options? : Options) :Status.Action] {
  const isPath = typeof path === 'string'&& path ! = =' ';

  const [status, setStatus] = useState<Status>(isPath ? 'loading' : 'unset');

  const [active, setActive] = useState(isPath);

  const ref = useRef<ExternalElement>();

  useEffect(() = > {
    // unmount the previous resourceref.current? .remove();if(! isPath || ! active) { setStatus('unset');
      ref.current = undefined;
      return;
    }

    setStatus('loading');
    // Create external element
    const pathname = path.replace(/ [r]. | # $/ *.' ');
    // Different types of resources are loaded in different ways
    if(options? .type ==='css' || /(^css! |\.css$)/.test(pathname)) {
      // css
      ref.current = document.createElement('link');
      ref.current.rel = 'stylesheet'; ref.current.href = path; ref.current.media = options? .media ||'all';
      // IE9+
      let isLegacyIECss = 'hideFocus' in ref.current;
      // use preload in IE Edge (to detect load errors)
      if (isLegacyIECss && ref.current.relList) {
        ref.current.rel = 'preload';
        ref.current.as = 'style';
      }
      ref.current.setAttribute('data-status'.'loading');
      document.head.appendChild(ref.current);
    } else if(options? .type ==='js' || /(^js! |\.js$)/.test(pathname)) {
      // javascript
      ref.current = document.createElement('script'); ref.current.src = path; ref.current.async = options? .async ===undefined ? true: options? .async; ref.current.setAttribute('data-status'.'loading');
      document.body.appendChild(ref.current);
    } else if(options? .type ==='img' || /(^img! |\.(png|gif|jpg|svg|webp)$)/.test(pathname)) {
      // image
      ref.current = document.createElement('img');
      ref.current.src = path;
      ref.current.setAttribute('data-status'.'loading');
      // append to wrapper
      constwrapper = (getTargetElement(options? .target)as HTMLElement) || document.body;
      if(wrapper) { wrapper.appendChild(ref.current); }}else{
      // do nothing
      console.error(
        "Cannot infer the type of external resource, and please provide a type ('js' | 'css' | 'img'). " +
          "Refer to the https://ahooks.js.org/hooks/dom/use-external/#options")}if(! ref.current)return

    // Bind setAttribute Event
    const setAttributeFromEvent = (event: Event) = >{ ref.current? .setAttribute('data-status', event.type === 'load' ? 'ready' : 'error');
    };
    ref.current.addEventListener('load', setAttributeFromEvent);
    ref.current.addEventListener('error', setAttributeFromEvent);
    const setStateFromEvent = (event: Event) = > {
      setStatus(event.type === 'load' ? 'ready' : 'error');
    };
    ref.current.addEventListener('load', setStateFromEvent);
    ref.current.addEventListener('error', setStateFromEvent);
    return () = >{ ref.current? .removeEventListener('load', setStateFromEvent); ref.current? .removeEventListener('error', setStateFromEvent);
    };
  }, [path, active]);

  const action = useMemo(() = > {
    const unload = () = > setActive(false);
    const load = () = > setActive(true);
    const toggle = () = > setActive((value) = >! value);return { toggle, load, unload };
  }, [setActive]);

  return [status, action];
}

Copy the code

useFavicon

import { useEffect } from 'react';

// image/vnd.microsoft.icon MIME type only works if the image is really an ICO file
// image/x-icon will also work with bitmaps and gifs
// Mainly for compatibility with non-ICO files with the extension ICO
const ImgTypeMap = {
  SVG: 'image/svg+xml'.ICO: 'image/x-icon'.GIF: 'image/gif'.PNG: 'image/png'};type ImgTypes = keyof typeof ImgTypeMap;

const useFavicon = (favUrl: string) = > {
  useEffect(() = > {
    if(! favUrl)return;

    const cutUrl = favUrl.split('. ');
    const imgSuffix = cutUrl[cutUrl.length - 1].toLocaleUpperCase() as ImgTypes;

    const link: HTMLLinkElement =
      document.querySelector("link[rel*='icon']") | |document.createElement('link');

    link.type = ImgTypeMap[imgSuffix];
    link.href = favUrl;
    // Most browsers will only recognize 'icon'. Only IE will recognize the entire name 'shortcut icon'.
    link.rel = 'shortcut icon';

    document.getElementsByTagName('head') [0].appendChild(link);
  }, [favUrl]);
};

export default useFavicon;

Copy the code

useFullscreen

The screenFull plugin wrapper

import { useCallback, useRef, useState } from 'react';
import screenfull from 'screenfull';
import useUnmount from '.. /useUnmount';
import { BasicTarget, getTargetElement } from '.. /utils/dom';

export interfaceOptions { onExitFull? :() = > void; onFull? :() = > void;
}

export default(target: BasicTarget, options? : Options) => {const { onExitFull, onFull } = options || {};
  /// store the event with ref
  const onExitFullRef = useRef(onExitFull);
  onExitFullRef.current = onExitFull;

  const onFullRef = useRef(onFull);
  onFullRef.current = onFull;

  const [state, setState] = useState(false);

  const onChange = useCallback(() = > {
    if (screenfull.isEnabled) {
      const { isFullscreen } = screenfull;
      if (isFullscreen) {
        onFullRef.current && onFullRef.current();
      } else {
        /// call off in the change event callback instead of directly in the exitFull method because the exit operation is just beginning and not finished yet
        screenfull.off('change', onChange); onExitFullRef.current && onExitFullRef.current(); } setState(isFullscreen); }} []);const setFull = useCallback(() = > {
    const el = getTargetElement(target);
    if(! el) {return;
    }
    // If full screen is supported, set it to full screen and listen for events of full screen status change
    if (screenfull.isEnabled) {
      try {
        screenfull.request(el as HTMLElement);
        screenfull.on('change', onChange);
      } catch (error) {}
    }
  }, [target, onChange]);

  const exitFull = useCallback(() = > {
    if(! state) {return;
    }
    /// Exit the full screen and clear the listener event
    if (screenfull.isEnabled) {
      screenfull.exit();
    }
  }, [state]);

  const toggleFull = useCallback(() = > {
    if (state) {
      exitFull();
    } else {
      setFull();
    }
  }, [state, setFull, exitFull]);

  useUnmount(() = > {
    if (screenfull.isEnabled) {
      screenfull.off('change', onChange); }});return [
    state,
    {
      setFull,
      exitFull,
      toggleFull,
    },
  ] as const;
};

Copy the code

useTitle

Manipulating the syntactic sugar for title

import { useEffect, useRef } from 'react';
import useUnmount from '.. /useUnmount';

export interfaceOptions { restoreOnUnmount? :boolean;
}

const DEFAULT_OPTIONS: Options = {
  restoreOnUnmount: false};function useTitle(title: string, options: Options = DEFAULT_OPTIONS) {
  // use ref to store the title before the change
  const titleRef = useRef(document.title);
  useEffect(() = > {
    document.title = title;
  }, [title]);

  useUnmount(() = > {
    if (options && options.restoreOnUnmount) {
      /// If the configuration needs to be restored, it will be restored when the component is uninstalled
      document.title = titleRef.current; }}); }export default typeof document! = ='undefined' ? useTitle : () = > {};

Copy the code

The resources

  • IntersectionObserver
  • Element.getBoundingClientRect()
  • screenfull

The above content due to my level problem is inevitable error, welcome to discuss feedback.