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

Today is the last chapter of the State section, so far all the 17 hooks of the State section have been interpreted. Thank you for taking the time to read 🙏 ~

useCountDown

“Full clearance, last three days, last three days…”

import { useEffect, useMemo, useState } from 'react';
import dayjs from 'dayjs';
import usePersistFn from '.. /usePersistFn';

/ / /...

/// The core method for calculating the remaining time
const calcLeft = (t? : TDate) = > {
  if(! t) {return 0;
  }
  
  Dayjs (t).valueof () = dayjs(t).valueof ()
  /// In addition, the calculation can be compatible with the following issue, because people who have not encountered this kind of problem may not know the existence of this pit
  /// the whole library of dayJS is introduced just to get a timestamp for a time. New Date indicates being touched
  // https://stackoverflow.com/questions/4310953/invalid-date-in-safari
  const left = dayjs(t).valueOf() - new Date().getTime();
  if (left < 0) {
    return 0;
  }
  return left;
};

/// format method
const parseMs = (milliseconds: number): FormattedRes= > {
  return {
    days: Math.floor(milliseconds / 86400000),
    hours: Math.floor(milliseconds / 3600000) % 24.minutes: Math.floor(milliseconds / 60000) % 60.seconds: Math.floor(milliseconds / 1000) % 60.milliseconds: Math.floor(milliseconds) % 1000}; };const useCountdown = (options? : Options) = > {
  const { targetDate, interval = 1000, onEnd } = options || {};

  const [target, setTargetDate] = useState<TDate>(targetDate);
  const [timeLeft, setTimeLeft] = useState(() = > calcLeft(target));

  const onEndPersistFn = usePersistFn(() = > {
    if(onEnd) { onEnd(); }});// calculate the remaining time according to the set interval
  useEffect(() = > {
    if(! target) {// for stop
      setTimeLeft(0);
      return;
    }

    // Execute immediately
    setTimeLeft(calcLeft(target));

    const timer = setInterval(() = > {
      const targetLeft = calcLeft(target);
      setTimeLeft(targetLeft);
      if (targetLeft === 0) {
        clearInterval(timer);
          onEndPersistFn();
      }
    }, interval);

    return () = > clearInterval(timer);
  }, [target, interval]);

  const formattedRes = useMemo(() = > {
    return parseMs(timeLeft);
  }, [timeLeft]);

  return [timeLeft, setTargetDate, formattedRes] as const;
};

export default useCountdown;

Copy the code

useHistoryTravel

“Once a true love lay before me…”

import { useState, useCallback, useRef } from 'react';

/ / /...
// get the final index in the array based on the specified number of steps
const dumpIndex = <T>(step: number, arr: T[]) = > {
  let index =
    step > 0
      ? step - 1 // move forward
      : arr.length + step; // move backward
  if (index >= arr.length - 1) {
    index = arr.length - 1;
  }
  if (index < 0) {
    index = 0;
  }
  return index;
};
/// The core method splits the specified array into past, present, and future parts based on the number of steps
const split = <T>(step: number, targetArr: T[]) = > {
  const index = dumpIndex(step, targetArr);
  return {
    _current: targetArr[index],
    _before: targetArr.slice(0, index),
    _after: targetArr.slice(index + 1)}; };export default function useHistoryTravel<T> (initialValue? : T) {
  const [history, setHistory] = useState<IData<T | undefined> > ({present: initialValue,
    past: [].future: []});const { present, past, future } = history;

  const initialValueRef = useRef(initialValue);
  Gone with the wind, tomorrow is another day
  const reset = useCallback(
    (. params:any[]) = > {
      const _initial = params.length > 0 ? params[0] : initialValueRef.current;
      initialValueRef.current = _initial;

      setHistory({
        present: _initial,
        future: [].past: []}); }, [history, setHistory] );/// There was no way, but there is a way
  const updateValue = useCallback(
    (val: T) = > {
      setHistory({
        present: val,
        future: [].past: [...past, present]
      });
    },
    [history, setHistory]
  );
  /// The past can not be recalled, the future can be chased
  const _forward = useCallback(
    (step: number = 1) = > {
      if (future.length === 0) {
        return;
      }
      const { _before, _current, _after } = split(step, future);
      setHistory({
        past: [...past, present, ..._before],
        present: _current,
        future: _after
      });
    },
    [history, setHistory]
  );
  /// I want to go back to find a person with seven moles on the soles of his feet
  const _backward = useCallback(
    (step: number = -1) = > {
      if (past.length === 0) {
        return;
      }

      const { _before, _current, _after } = split(step, past);
      setHistory({
        past: _before,
        present: _current,
        future: [..._after, present, ...future]
      });
    },
    [history, setHistory]
  );
  /// The Time Lord is me
  const go = useCallback(
    (step: number) = > {
      const stepNum = typeof step === 'number' ? step : Number(step);
      if (stepNum === 0) {
        return;
      }
      if (stepNum > 0) {
        return _forward(stepNum);
      }
      _backward(stepNum);
    },
    [_backward, _forward]
  );

  return {
    value: present,
    setValue: updateValue,
    backLength: past.length,
    forwardLength: future.length,
    go,
    back: useCallback(() = > {
      go(-1);
    }, [go]),
    forward: useCallback(() = > {
      go(1);
    }, [go]),
    reset
  };
}

Copy the code

useNetwork

“How’s your Internet speed?”

import { useEffect, useState } from 'react';

/ / /...
/// depends on window.navigator
function getConnection() {
  const nav = navigator as any;
  if (typeofnav ! = ='object') return null;
  return nav.connection || nav.mozConnection || nav.webkitConnection;
}

function getConnectionProperty() :NetworkState {
  const c = getConnection();
  if(! c)return {};
  return {
    rtt: c.rtt,
    type: c.type,
    saveData: c.saveData,
    downlink: c.downlink,
    downlinkMax: c.downlinkMax,
    effectiveType: c.effectiveType,
  };
}

function useNetwork() :NetworkState {
  const [state, setState] = useState(() = > {
    return {
      since: undefined.online: navigator.onLine, ... getConnectionProperty(), }; }); useEffect(() = > {
    const onOnline = () = > {
      setState((prevState) = > ({
        ...prevState,
        online: true.since: new Date()})); };const onOffline = () = > {
      setState((prevState) = > ({
        ...prevState,
        online: false.since: new Date()})); };const onConnectionChange = () = > {
      setState((prevState) = >({... prevState, ... getConnectionProperty(), })); };/// listen for a bunch of events to listen for network fluctuations
    window.addEventListener('online', onOnline);
    window.addEventListener('offline', onOffline);

    constconnection = getConnection(); connection? .addEventListener('change', onConnectionChange);

    return () = > {
      window.removeEventListener('online', onOnline);
      window.removeEventListener('offline', onOffline); connection? .removeEventListener('change', onConnectionChange); }; } []);return state;
}

export default useNetwork;

Copy the code

useWebSocket

“Hey, hey, hey, where are you?”

import useUnmount from '.. /useUnmount';
import usePersistFn from '.. /usePersistFn';

import { useEffect, useRef, useState } from 'react';

export enum ReadyState {
  Connecting = 0,
  Open = 1,
  Closing = 2,
  Closed = 3,}/ / /...

export default function useWebSocket(socketUrl: string, options: Options = {}) :Result {
  const {
    reconnectLimit = 3,
    reconnectInterval = 3 * 1000,
    manual = false,
    onOpen,
    onClose,
    onMessage,
    onError,
  } = options;

  const reconnectTimesRef = useRef(0);
  const reconnectTimerRef = useRef<NodeJS.Timeout>();
  const websocketRef = useRef<WebSocket>();

  const [latestMessage, setLatestMessage] = useState<WebSocketEventMap['message'] > ();const [readyState, setReadyState] = useState<ReadyState>(ReadyState.Closed);

  /** * reconnect */
  const reconnect = usePersistFn(() = > {
    if( reconnectTimesRef.current < reconnectLimit && websocketRef.current? .readyState ! == ReadyState.Open ) { reconnectTimerRef.current &&clearTimeout(reconnectTimerRef.current);
      // keep trying until the connection number reaches the maximum
      reconnectTimerRef.current = setTimeout(() = >{ connectWs(); reconnectTimesRef.current++; }, reconnectInterval); }});/// Connect to WS and register a bunch of events to synchronize the state
  const connectWs = usePersistFn(() = > {
    reconnectTimerRef.current && clearTimeout(reconnectTimerRef.current);

    if (websocketRef.current) {
      websocketRef.current.close();
    }

    try {
      websocketRef.current = new WebSocket(socketUrl);
      websocketRef.current.onerror = (event) = >{ reconnect(); onError && onError(event); setReadyState(websocketRef.current? .readyState || ReadyState.Closed); }; websocketRef.current.onopen =(event) = > {
        onOpen && onOpen(event);
        reconnectTimesRef.current = 0; setReadyState(websocketRef.current? .readyState || ReadyState.Closed); }; websocketRef.current.onmessage =(message: WebSocketEventMap['message']) = > {
        onMessage && onMessage(message);
        setLatestMessage(message);
      };
      websocketRef.current.onclose = (event) = >{ reconnect(); onClose && onClose(event); setReadyState(websocketRef.current? .readyState || ReadyState.Closed); }; }catch (error) {
      throwerror; }});/** * Send message *@param message* /
  const sendMessage: WebSocket['send'] = usePersistFn((message) = > {
    if(readyState === ReadyState.Open) { websocketRef.current? .send(message); }else {
      throw new Error('WebSocket disconnected'); }});/**
   * 手动 connect
   */
  const connect = usePersistFn(() = > {
    reconnectTimesRef.current = 0;
    connectWs();
  });

  /** * disconnect websocket */
  const disconnect = usePersistFn(() = > {
    reconnectTimerRef.current && clearTimeout(reconnectTimerRef.current); reconnectTimesRef.current = reconnectLimit; websocketRef.current? .close(); }); useEffect(() = > {
    // Initial connection
    if(! manual) { connect(); } }, [socketUrl, manual]); useUnmount(() = > {
    disconnect();
  });

  return {
    latestMessage,
    sendMessage,
    connect,
    disconnect,
    readyState,
    webSocketIns: websocketRef.current,
  };
}

Copy the code

useWhyDidYouUpdate

“Show me yue Kang code”

Log each change to the props property

import { useEffect, useRef } from 'react';

export type IProps = {
  [key: string]: any;
};

export default function useWhyDidYouUpdate(componentName: string, props: IProps) {
  const prevProps = useRef<IProps>({});

  useEffect(() = > {
    if (prevProps.current) {
      const allKeys = Object.keys({ ... prevProps.current, ... props });const changedProps: IProps = {};

      allKeys.forEach((key) = > {
        / / / to use! The = = comparison
        if(prevProps.current! [key] ! == props[key]) { changedProps[key] = {from: prevProps.current! [key],to: props[key], }; }});if (Object.keys(changedProps).length) {
        console.log('[why-did-you-update]', componentName, changedProps);
      }
    }

    prevProps.current = props;
  });
}

Copy the code

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