18 user defined react hooks

Hu Zhiwu 2021-04-07 20:37:25
user defined react hooks


1. useCreation

useCreation yes useMemo and useRef substitute , Better performance

function fn(){
  const a = useRef(new Subject())// fn Every re rendering creates Subject example
  //  No matter what fn Re render a few times ,useCreation Will judge whether dependence changes ,
  // Then decide whether to carry out factory function ( The first parameter )
  const a = useCreation(()=>new Subject(),[deps])
}

Realization useCreation

  1. Determine the input and output ,useCreation Take two parameters , A factory function , An array of dependencies , And returns the result of factory function execution
//   Use generics T Constrained useCreation The result returned must be consistent with what the factory function returns 
function useCreation<T>(factory:()=>T,deps:DependencyList[]):T;
  1. analysis , When the component is re rendered , You need to determine if the dependency has changed and re execute factory function , Then we can know the dependencies and factory The returned content needs to be persistent .factory Functions are only executed when the dependency changes and the first rendering , You also need to know useCreation Whether it has been initialized
function useCreation<T>(factory:()=>T,deps:DependencyList[]):T{
    const {current}=useRef({
    obj:undefined as undefined | T,/ factory The returned content is stored in obj in
    deps,/
/  Dependencies
    initialized:false/
/ Whether to initialize
  })
}
  1. Determine if the dependencies are the same
function depsAreSame(oldDeps:any[],deps:DependencyList[]):boolean{
 if(oldDeps===deps){
   return true;
  }
  for(const i in oldDeps){
   if(oldDeps[i]!==deps[i]){
     return false
    }
  }
  return true;
}
  1. Initialization and dependency changes , To perform factory
if(!current.initialized||!depsAreSame(current.deps,deps)){
  current.obj = factory()
  current.deps=deps;
  current.initialized=true
}
  1. Complete code
function useCreation<T>(factory:()=>T,deps:any[]):T{
  const {current} = useRef({
    obj:undefined as undefined | T,
    initialized:false,
    deps,
  })
  
  if(!current.initialized||depsAreSame(current.deps,deps)){
    current.obj = factory()
    current.initialized=true;
    current.deps = deps;
  }
  return current.obj as T
}

function depsAreSame(oldDeps:any[],deps:any[]):boolean{
 if(oldDeps===deps){
   return true;
  }
  for(const i in oldDeps){
   if(oldDeps[i]!==deps[i]){
     return false;
    }
  }
  return true;
}

2. useDebounceFn

It's used for function anti shake hook

Function anti shake is similar to the switch of elevator door , The elevator door will wait normally 10s After closing , But if you trigger the opening and closing mechanism of the elevator door before closing , Then the elevator door will refresh the waiting time , Wait again 10 Seconds to close Realization

  1. The first edition
type Fn = (...args: any) => any
export default function DebounceFn<T extends Fn>(func: T, wait: number{
    let timeout: NodeJS.Timeout;
    return function ({
        if (timeout) {
            clearTimeout(timeout)
        }
        timeout = setTimeout(func, wait)
    }
}

The first edition was rather crude , You can find , Lack of this The direction of , Parameter passing , The return value of the function , We should actually make sure that , The function returned should be the same as the function passed in func Agreement , After all, function anti dithering just changes the timing of function execution , But you should not change the parameters of the function and the internal implementation mechanism

  1. The second edition
//  Returns the parameter type of the function 
type ArgumentsTypes<F extends Function> = F extends (...args: infer A) => any ? A : never;

//  Define the type of function passed in
type Fn = (...args: any) => any

// The function type returned after anti dithering
type ReturnFn<K extends Fn> = (...args: ArgumentsTypes<K>) => ReturnType<K>


export default function DebounceFn<K extends Fn>(fn: K, wait: number): ReturnFn<K{
    let timeout: NodeJS.Timeout
    // ReturnType<K>  Define the return value type of the function
    let result: ReturnType<K>
    return function (thisany, ...args: ArgumentsTypes<K>{
        if (timeout) {
            clearTimeout(timeout)
        }
        timeout = setTimeout(() => {
           //  It's solved this Direction problem of , And parameter transfer
            result = fn.apply(this, args)
        }, wait)

       //  Returns the return value of the function
        return result;
    }
}

image.png image.png This edition , We solved this Point to the problem , Parameter passing problem , Function return value problem , And with the help of TS Complete the type derivation of the function . Now we're going to add a feature , That is, every time an event is triggered , The anti shake function is based on immediate To determine whether to execute immediately image.png If immediate yes true, It's in wait At the beginning of , also wait No more execution during this period

  1. The third edition
type ArgumentsTypes<F extends Function> = F extends (...args: infer A) => any ? A : never;


type Fn = (...args: any) => any

type ReturnFn<K extends Fn> = (...args: ArgumentsTypes<K>) => ReturnType<K>

export default function DebounceFn<K extends Fn>(fn: K, wait: number, immediate: boolean): ReturnFn<K{
    let timeout: NodeJS.Timeout | null
    let result: ReturnType<K>
    return function (thisany, ...args: ArgumentsTypes<K>{

        const later = () => {
            // wait After the end ,timeout The assignment is null
            //  Mark another wait The beginning of
            timeout = null
            // wait After the end , immediate:false, Execute function
            if (!immediate) {
                result = fn.apply(this, args)
            }
        }
        // immediate:true  Function to be executed immediately  
        if (immediate) {
            //  No timer means wait The beginning of
            if (!timeout) {
                //  to timeout assignment , Show that you are not wait The beginning of , Get into wait Inside
                timeout = setTimeout(() => {
                    later()
                }, wait)
                result = fn.apply(this, args)
            }
        } else {
            // immediate:false
            //  Every time a trigger , Then clear the previous timer , Start a new timer
            if (timeout) {
                clearTimeout(timeout)
            }
            timeout = setTimeout(() => {
                later()
            }, wait)
        }
        return result;
    }
}
  1. The Fourth Edition , Add a function to cancel the current anti shake
export default function DebounceFn<K extends Fn>
  (fn: K, wait: number, immediate: boolean): ReturnFn<K> & 
{ cancel: () => void } 
{
    let timeout: NodeJS.Timeout | null
    let result: ReturnType<K>
    function _debounce(thisany, ...args: ArgumentsTypes<K>{


        const later = () => {
            // wait After the end ,timeout The assignment is null
            //  Mark another wait The beginning of
            timeout = null
            // wait After the end , immediate:false The execution function of
            if (!immediate) {
                result = fn.apply(this, args)
            }
        }
        // immediate:true  Function to be executed immediately  
        if (immediate) {
            //  No timer means wait The beginning of
            if (!timeout) {
                //  to timeout assignment , Show that you are not wait The beginning of , Get into wait Inside
                timeout = setTimeout(() => {
                    later()
                }, wait)
                result = fn.apply(this, args)
            }
        } else {
            // immediate:false
            //  Every time a trigger , Then clear the previous timer , Start a new timer
            if (timeout) {
                clearTimeout(timeout)
            }
            timeout = setTimeout(() => {
                later()
            }, wait)
        }
        return result;
    }
    _debounce.cancel = function ({
        if (timeout) {
            clearTimeout(timeout)
        }
    }
    return _debounce
}

Function anti shake is done , But in a function component , Each rendering will regenerate an anti shake function , Too much performance , We use hook Fix the address of the generated anti shake function .

  1. The fifth edition , coordination hook
export function useDebounceFn<T extends Fn>(fn: T, wait: number, immediate: boolean{
    const fnRef = useRef<T>(fn)
    fnRef.current = fn

    const debounce = useCreation(() => {
        return DebounceFn(fnRef.current, wait, immediate)
    }, [])

    return {
        run: debounce as any as T,
        cancel: debounce.cancel
    }
}

image.png image.png

3.useDebounce

Used to deal with the anti shake value hook useDebounceFn It's the anti shake of the function ,useDebounce It's the anti shake of the value

Realization

  1. Confirm the input , Output . It's not only an anti shake to the value , Then enter value,wait,immediate, The output is value
export default function useDebounceFn<T>(value: T, wait: number, immediate: boolean):T {}
  1. The internal statement is that state To store the value of anti shake processing
export default function useDebounce<T>(value: T, wait: number, immediate: boolean): T {
    const [state, setState] = useState<T>(value)
    const { run } = useDebounceFn(() => {
        setState(value)
    }, wait, immediate)
    return state;
}
  1. Monitor the outside value The change of , And call run
export default function useDebounce<T>(value: T, wait: number, immediate: boolean): T {
    const [state, setState] = useState<T>(value)

    const { run } = useDebounceFn(() => {
        setState(value)
    }, wait, immediate)

    useEffect(() => {
        run()
    }, [value])
    return state;

}

4. useInterval

One can handle setInterval Of hook

export default function(){
 const [num,setNum] = useState(0)
  
  useEffect(()=>{
   setInterval(()=>{
     setNum(num+1)
    },1000)
  },[])
}

In the above code , I thought every second num It will increase 1, But the actual run time , No matter how many seconds ,num It will only increase to 1 Then it stopped . This is because in the setInterval In Chinese num, It's in the initial context num=0, So it will be repeated all the time setNum(0+1) For normal use setInterval, We just need to re render the component , to setInterval Pass in the latest execution function Realization

  1. Confirm the input , Output , Input : A function that needs to be executed fn, Timer time wait, Whether to execute immediately immediate, No output
interface IOptions {
    immediate: boolean
}
export default function useInterval(fn: () => void, wait: number, { immediate }: IOptions):void;
  1. In order to be in setInterval Execute the latest function in , We need to use useRef. also setInterval It is usually executed after the component is rendered , So we need useEffect
export default function useInterval(fn: () => void, wait: number, { immediate }: IOptions{
    const fnRef = useRef(fn)
    fnRef.current = fn
    useEffect(() => {
        setInterval(fnRef.current, wait)
    }, [wait])
}
  1. In addition, the timer is cleared and executed immediately when the component is unloaded
export default function useInterval(fn: () => void, wait: number, { immediate }: IOptions{
    const fnRef = useRef(fn)
    fnRef.current = fn
    let timer: NodeJS.Timeout
    useEffect(() => {
        // immediate:true It means to execute immediately
        if (immediate) {
            fnRef.current()
        }
        timer = setInterval(fnRef.current, wait)

        //  Don't forget to clear the timer when the component is unloaded
        return () => {
            clearTimeout(timer)
        }

    }, [wait])
}

5.useEventEmitter

Notification of events between multiple components can be a headache , With the help of EventEmitter , It can make the process easier .

EventEmitter It's usually implemented with classes , There are three attribute methods inside , A property stores the events of the subscription , One way to subscribe to events , A method triggers an event Realization :

//  Define the event type of the subscription 
type SubScription<T> = (val: T) => void
class EventEmitter<T>{
    //  Define a private property , Used to store subscription Events
    // set You can guarantee that you will not subscribe to repeated events
    private subscriptions = new Set<SubScription<T>>()

    //  Subscription events
    useSubScription = (callback: SubScription<T>) => {
        //  Use ref It is guaranteed that when an event is executed , The function is up to date ,
        // useEffect The dependency of is an empty array , Use ref, It can be guaranteed in useEffect The events executed in are up-to-date
        const callbackRef = useRef<SubScription<T>>()
        callbackRef.current = callback

        useEffect(() => {
          //  Add a layer of judgment , When a function that subscribes to an event exists , To perform
            function subscription(val: T{
                if (callbackRef.current) {
                    callbackRef.current(val)
                }
            }
            //  Subscription events
            this.subscriptions.add(subscription)

            //  When components are destroyed , Delete subscription Events
            return () => {
                this.subscriptions.delete(subscription)
            }
            //  No matter how the component is rendered , Registration events , Only once
        }, [])
    }


    //  Triggering event
    //  Be careful T
    //  The parameter type of the event is T, And useSubScription The parameter types of subscribed functions are consistent
    emit = (val: T) => {
        //  Traversal Events
        for (const subscription of this.subscriptions) {
            subscription(val)
        }
    }
}

//  because EventEmitter It's a class , Function components render each time , Will generate a new object ,
//  So you need to use ref
export default function useEventEmitter<T>({
    const eventEmitterRef = useRef<EventEmitter<T>>()
    if (!eventEmitterRef.current) {
        eventEmitterRef.current = new EventEmitter()
    }
    return eventEmitterRef.current
}

6. useLock

Used to add race lock to an asynchronous function , Prevent concurrent execution .

Achieve this , Just use ref To store the switch of the lock , When the function starts executing , Close the lock , After execution , Lock open . The lock is off and does not trigger function execution

export function useLockFn<T extends any[], K>(fn: (...args: T) => Promise<K>): (...args: T) => Promise<K | undefined{

  const lockRef = useRef(false)

  return useCallback(async (...args: T) => {
      if (lockRef.current) return

      try {
          lockRef.current = true;
          const result = await fn(...args)
          return result
      } catch (e) {
          throw e

      } finally {
          lockRef.current = false
      }
  }, [fn])
}

7.useReactive

Provide a data responsive operation experience , You don't need to write to define data state useState , Directly modify the properties to refresh the view .

  1. Get some background Reflect.get(target, name, receiver)

Reflect.get Method to find and return target Object's name attribute , If this property is not available , Then return to undefined.

var myObject = {
  foo: 1,
  bar: 2,
  get baz() {
    return this.foo + this.bar;
  },
}
Reflect.get(myObject, 'foo') // 1
Reflect.get(myObject, 'bar') // 2
Reflect.get(myObject, 'baz') // 3

If name Property deploys the read function (getter), Then read the this binding receiver.

var myObject = {
  foo: 1,
  bar: 2,
  get baz() {
   //  there this Refer to ,Reflect.get Medium receiver
    return this.foo + this.bar;
  },
};
var myReceiverObject = {
  foo: 4,
  bar: 4,
};
Reflect.get(myObject, 'baz', myReceiverObject) // 8
  1. In order to achieve data responsive , We need to use Proxy and Reflect Create an observer , Proxy data
function Observer<T extends Object>(initState:T,cb:()=>void):T{
  const proxy = New Proxy<T>(initState,{
   get(target,prop,receiver){
     return Reflect.get(target,prop,receiver)
    },
    
    set(target,prop,value){
     const ret = Reflect.set(target,prop,value)
      //  Each assignment calls a callback function
      cb();
      return ret;
    },
    deleteProperty(target,key){
     const ret = Reflect.deleteProperty(target,key)
      cb();
      return ret;
    }
  })
  return proxy;
}
  1. Data can be a multi-level object , So you need to recursively proxy the data
function isObject<T extends Object>(val:T):boolean{
 return typeof val==="Object"&&val!==null
}

function Observer<T extends Object>(initState:T,cb:()=>void):T{
  const proxy = New Proxy<T>(initState,{
   get(target,prop,receiver){
      //  Determine whether the attribute of the proxy is an object , Yes, recursive proxy
      // receiver Refer to proxy example , The property obtained at that time was a function , And the function uses this when ,this Refer to receiver
        const ret = Reflect.get(target,prop,receiver)
      
     return isObject(ret)?Observer(ret,cb):Reflect.get(target,prop)
    },
    
    set(target,prop,value){
     const ret = Reflect.set(target,prop,value)
        //  Each assignment calls a callback function
        cb();
        return ret;
    },
    
    deleteProperty(target,key){
     const ret = Reflect.deleteProperty(target,key)
        cb();
        return ret;
    }
  })
  return proxy;
}
  1. It's just a proxy for the data , But even if the data changes ,react The component does not re render , So reserved cb function , When data changes , Refresh component
export default function useReactive<S extends Object>(state:S):S{
 //  Forced to refresh  
  const [,forceUpdate] = useState({})
  
  //  Every time a function component is re rendered and executed , Will bring in New state, This leads to a new one every time state Acting as agent
  //  So it needs to be persistent state
  const stateRef = useRef(state)
  
  return useMemo(()=>Observer(stateRef.current,()=>{
    //  Every time the data is assigned a value , Then force the component to refresh
   forceUpdate()
  }),[])
}
  1. Optimize , Need to prevent duplicate agents , As well as preventing the proxy from representing objects that have already been represented
// k:v  The original object : Represented objects 
const proxyMap = new WeakMap();
// k:v  Represented objects : The original object
const rawMap = new WeakMap();

function Observer<T extends Object>(initState: T, cb: () => void): T {
    const existingProxy = proxyMap.get(initState)
    //  I've been acting for , Then we don't repeat the proxy
    if (existingProxy) {
        return existingProxy
    }

    //  Prevent proxy objects that have already been proxy
    if (rawMap.has(initState)) {
       return initState
    }
    const proxy = new Proxy<T>(initState, {
        get(target, prop, receiver) {
            //  Determine whether the attribute of the proxy is an object , Yes, recursive proxy
            // receiver Refer to proxy example , The property obtained at that time was a function , And the function uses this when ,this Refer to receiver
            const ret = Reflect.get(target, prop, receiver)

            return isObject(ret) ? Observer(ret, cb) : Reflect.get(target, prop)
        },

        set(target, prop, value) {
            const ret = Reflect.set(target, prop, value)
            //  Each assignment calls a callback function
            cb();
            return ret;
        },
        deleteProperty(target,key){
            const ret = Reflect.deleteProperty(target,key)
            cb();
            return ret;
       }
    })
    
    proxyMap.set(initState,proxy)
   rawMap.set(proxy,initState)


    return proxy;
}

8. useTrackedEffect

stay  useEffect  On the basis of , Tracking triggers effect Dependence on change .

** Realization **

  1. Determine the input and output ,
// changeIndex The change depends on the index 
type Effect = (changeIndex: number[], previousDeps: DependencyList | undefined, currentDeps: DependencyList) => any
// effect: Side effect function
// deps: Dependency array
export default function useTrackedEffect(effect: Effect, deps: DependencyList{
  1. Want to know which of the dependency arrays is changing , We need to write a function to determine
const diffTwoDeps = (preDeps:DependencyList|undefined,curDeps:DependencyList)=>{
    //  When the component is initialized ,pre Obviously it doesn't exist ,
    return preDeps
        ? preDeps.map((item,index)=>curDeps[index]!==item?index:-1).filter(item=>item>0)
   : curDeps
        ? curDeps.map((item,index)=>index)
   :[]
}
  1. useTrackedEffect In essence useEffect, And to compare before and after deps The change of , You need help ref
export default function useTrackedEffect(effect:Effect,deps:DependencyList){
  //  To compare the changes in dependencies , Dependencies must be persisted , For comparison
  const previousDepsRef = useRef<DependencyList>()
  
  useEffect(()=>{
    const changeIndex = diffTwoDeps(previousDepsRef.current,deps)
    const previousDeps = previousDepsRef.current
    previousDepsRef.current = deps
    return effect(changeIndex,previousDeps,deps)
  },deps)
}

9.useUpdateEffect

One that is executed only when dependent on updates useEffect hook.

import { useEffect, useRef, DependencyList, EffectCallback } from "react";

export default function useUpdateEffect(effect: EffectCallback, deps: DependencyList{
    const isMount = useRef(true);

    useEffect(() => {
        if (!isMount.current) {
            //  Remember to return 
            return effect()
        } else {
            isMount.current = false
        }
    }, deps)
}

10.useControllableValue

In some component development , We need the state of the component to be self managed , It can also be controlled externally ,useControllableValue Is to help you manage this state Hook.

Realization :

  1. Use :
const ControllableComponent = (props: any) => {
  const [state, setState] = useControllableValue<string>(props);
}
  1. Determine the input and output :
//  Passed by the parent component Props
interface Props {
    [key: string]: any
}
interface IOptions<T> {
    defaultValue?: T // The default value of the component itself
    valuePropName?: string //  Defines the property name of the value passed by the parent component
    defaultPropName?: string //  The property name of the default value passed by the parent component
    trigger?: string //  When modifying a value , Functions passed by the triggering parent component ,
}
export default function useControllableValue<T>(props: Props, options: IOptions<T>{
    const {
        defaultValue,
        defaultPropName = "defaultValue",
        valuePropName = "value",
        trigger = "onChange"
    } = options
}
  1. analysis :
    1. The state can be controlled by the parent component , It can also be controlled by the component itself
    2. It can be obtained. , You need to get the props
    3. The parent component needs full control value, that value What is the property name of ,valuePropName="value"
    4. The parent component just passes a default value , So what is the property name of the default value ,defaultPropName="defaultValue"
    5. The parent component needs to know the change in value , You need to execute a callback function , So what is the property name of the callback function :trigger="onChange"
    6. The component itself needs a default value :defaultValue
  2. The priority of States is passed in by the parent component value> The parent component passed in defaultValue> The default value of the component itself
export default function useControllableValue<T>(props:Props,options:IOptions<T>){
    const {
        defaultValue,
        defaultPropName="defaultValue",
        valuePropName="value",
        trigger="onChange"
     } = options
  
  //  Get the value passed in by the parent component
  const value = props[valuePropName]
  
  const [state,setState] = useState(()=>{
    //  The default value passed in by the parent component
    if(defaultPropName in props){
     return props[defaultPropName]
    }
    //  The default value of the component itself
    return defaultValue
  })
}
  1. When updating the status , You need to determine whether a component is a controlled component or an uncontrolled component , The controlled component calls props.trigger, Uncontrolled components call setState
const handleSetState = useCallback((e:T,...args:any[]){
 //  If valuePropName non-existent , Then the component is an uncontrolled component
        if(!props[valuePropName]){
            setState(e)
 }

 //  If trigger There is , Then the component is a controlled component
 if(props[trigger]){
            props[trigger](
                e,
              ...args
            )
         }
},[valuePropName,trigger,props])
  1. Complete code
interface Props {
    [key: string]: any
}
interface IOptions<T> {
    defaultValue?: T
    valuePropName?: string
    defaultPropName?: string
    trigger?: string
}
export default function useControllableValue<T>(props: Props, options: IOptions<T>{
    const {
        defaultValue,
        defaultPropName = "defaultValue",
        valuePropName = "value",
        trigger = "onChange"
    } = options

    //   Get the value passed in by the parent component
    const value = props[valuePropName]

    const [state, setState] = useState<T | undefined>(() => {

        //  The default value passed in by the parent component
        if (defaultPropName in props) {
            return props[defaultPropName]
        }
        //   The default value of the component itself
        return defaultValue
    }
)

    const handleSetState = useCallback((e: T, ...args: any[]) => {
        //  without valuePropName  Prove to be an uncontrolled component
        if (!(valuePropName in props)) {
            setState(e)
        }
        if (props[trigger]) {
            props.trigger(e, ...args)
        }
    }, [trigger, props, valuePropName]
)

    return [valuePropName in props ? value : statehandleSetStateas const
 }

11. useMap

One can manage Map Of type state Hook.

import { useState, useMemo } from "react";

//  As long as there is Iterable Interface can do map Parameters of
export default function useMap<KT>(initState?: Iterable<readonly [K, T]>{
    //  Save the default values
    const initMap = useMemo(() => {
        return initState ? new Map(initState) : new Map()
    }, [initState])

    const [map, setMap] = useState<Map<K, T>>(initMap)

    const stableActions = useMemo(() => ({
        remove(key: K) {
            setMap(pre => {
                const map = new Map(pre)
                map.delete(key)
                return map;
            })
        },
        setAll(state: Iterable<readonly [K, T]>) {
            const newMap = new Map(state)
            setMap(newMap)
        },
        set(key: K, value: T) {
            setMap(pre => {
                const map = new Map(pre)
                map.set(key, value);
                return map
            })
        },
        reset() {
            setMap(initMap)
        }
    }), [setMap, initMap])

    const utils = {
        get(key: K) => map.get(key),
        ...stableActions
    }

    return [map, utils] as const
}

12. getTargetElement

Can get dom Methods

demand

  1. This method can receive a function , Used to get dom ()=>getElementsByClassName("abc")
  2. This method can accept one dom,
  3. This method can accept one dom Of ref

Sum up , You can define a base type

type BasicTarget<T=HTMLElement>= 
                 | (()=>T|null)//  After a function is executed , Return to one dom|null
                 | T // dom
                 | null 
                 | MutableRefObject<T | null | undefined>// dom Of ref

Make it better ,T Not only is HTMLElement, It can also be  | Element| Document | Window | HTMLElement

type TargetElement = | Element | Document | Window | HTMLElement

Realization

  1. Determine the input and output :
export default function getTargetElement

//defaultTarget Is in target by null when , Default returned
(target?:BasicTarget<TargetElement>,defaultTarget?:TargetElement)

//  The function finally returns
:TargetElement|null|undefined
  1. Judge target The type of , And deal with it accordingly
export default function getTargetElement(target?:BasicTarget<TargetElement>,defaultTarget?:TargetElement):TargetElement|null|undefined{
 
  //  If target non-existent , Then return to the default dom
  if(!target){
   return defaultTarget
  }
  let targetElement:TargetElement|null|undefined
  
  //  If target Is a function , Then execute the function
  if(typeof target === "function"){
   targetElement = target()
    // If target yes ref , Then return to ref.current
  }else if ("current" in target){
   targetElement = target.current
  }else{
   targetElement = target
  }
  
  return targetElement;
}

13. useClickAway

Elegant management of target elements outside click events Hook.

demand  :

  1. Trigger outside the target area dom When an event is , Trigger callback function
  2. From the above, the parameters need to be Callback function  , dom , event

Realization :

  1. Determine the input and output
//  Define default Events   mouse click
const defaultEvent = "click"

//  Define event types , Browser mouse events , Touch event of mobile terminal
type EventType = MouseEvent | TouchEvent

export default function useClickAway(
  onClickAway:(e:EventType)=>void,
  target:BasicTarget|BasicTarget[],//  The goal is dom, The goal is dom Can be more
  eventName:string = defaultEvent//  Listening events
)
  1. If you need to monitor the target dom Events outside the region , You need to use an event delegate , stay document Monitoring events on the Internet ( Note the need for dom After mounting , Monitor again , Need to use useEffect)
export default function useClickAway(
 onClickAway:(e:EventType)=>void,
  target:BasicTarget|BasicTarget[],//  The goal is dom
  eventName:string = defaultEvent//  Listening events
)
{
  const onClickAwayRef = useRef(onClickAway)
  onClickAwayRef.current = onClickAway
  
  useEffect(()=>{
    const handler = ()=>{}
    
    document.addEventListener(eventName,handler)
    //  Remember to delete the event delegate , Avoid memory leaks
    return ()=>{
     document.removeEventListener(eventName,handler)
    }
  },[eventName,target])
}
  1. handler Every time called , We just need to determine the source of the event dom Whether it's the goal or not dom in , In this case, it will not be carried out , If you're not there, do it
const handler = (event:any)=>{
    const targetArray = Array.isArray(target)?target:[target]
    
    if(
   targetArray.some(item=>{
      //  Get dom
     const targetElement = getTargetElement(item) as HTMLElement;
      //  The goal is dom There is no existence or goal dom Containing the event source that triggers the event dom, Do not perform
      return !targetElement || targetElement.contains(event.target)})
    ){
     return;
    }
    onClickAwayRef.current(event)
}

14.useSessionStorage

have access to sessionStorage Of hook

analysis :

  1. sessionStorage The change in the world will not make react Component re render , So we need to use useState
  2. When to use sessionStorage? On initialization , take sessionStorage Assign a value to state
  3. Additions and deletions ,sessionStorage and state Just synchronize ,
  4. return state

Realization :

  1. Determine the input and output
export default useSessionStorage<T>(
  key:string,
  defaultValue?:T
):[state,updateState] as const
  1. On initialization , Use sessionStorage to state assignment
const [state,setState] = useState<T|undefined>(()=>getStoreValue()

function getStoreValue(){
  const raw = sessionStorage.getItem(key)
  if(raw){
   try{
     return JSON.parse(raw)
    }catch(err){}
  }else{
   return defaultValue
  }
}
  1. to update session
const updateState = useCallback((newState?:T)=>{
  if(typeof newState === "undefined"){
    sessionStorage.removeItem(key)
    setState(undefined)
  }else{
    sessionStorage.setItem(kef,JSON.stringify(newState))
    setState(newState)
  }
},[key])
  1. useSessionStorageState You can also use function updater, It's like useState like that .
interface IFuncUpdater<T>{
 (previousState?:T):T
}
//  Why is this obj is T   instead of boolean
// obj is T  When established ,obj You can call T Methods and properties in types
//  and boolean No way.
function isFunction<T>(obj:any):obj is T{
 return typeof obj==="fucntion"
}

const updateState = useCallback((value?:T|IFuncUpdater<T>)=>{
  if(typeof newState === "undefined"){
    sessionStorage.removeItem(key)
    setState(undefined)
  }esle if (isFunction<IFuncUpdater<T>>(value)){
    const previousState = getStoreValue()
    //  Will the last value In the incoming function
    const newState = value(previousState)
    sessionStorage.setItem(key,JSON.string(newState))
    setState(newState)
  }else{
    sessionStorage.setItem(key,JSON.stringify(newState))
    setState(newState)
  }
},[key])

15 useEventListener

Elegant use addEventListener Of Hook.

Realization

  1. Determine the input and output

Native addEventListener Three parameters are generally required , Bound event name , Event handler , The goal is dom, therefore useEventListener We also need these three parameters

type BasicTarget<T = HTMLElement> =
    | T
    | null
    | (() => T | null)
    | MutableRefObject<T | null | undefined>

export default function useEventListener(eventName: string, handler: Function, target: BasicTarget) { }

You can see , Although I know the input and output for you , however , The type restrictions on parameters are too weak ,eventName Cannot ensure that the event type name entered by the user is correct ,handler There is no corresponding parameter transfer type prompt

  1. Parameter constraints
import { MutableRefObject } from "react";

type BasicTarget<T = HTMLElement> =
    | T
    | null
    | (() => T | null)
    | MutableRefObject<T | null | undefined>;
    
type Target = BasicTarget<HTMLElement | Window | Document | Element>

function useEventListener<K extends keyof HTMLElementEventMap>(
    eventName: K,
    handler: (e: HTMLElementEventMap[K]) => void,
    target: BasicTarget<HTMLElement>
): void;

function useEventListener<K extends keyof WindowEventMap>(
    eventName: K,
    handler: (e: WindowEventMap[K]) => void,
    target: BasicTarget<Window>
): void;

function useEventListener<K extends keyof ElementEventMap>(
    eventName: K,
    handler: (e: ElementEventMap[K]) => void,
    target: BasicTarget<Element>
): void;

function useEventListener<K extends keyof DocumentEventMap>(
    eventName: K,
    handler: (e: DocumentEventMap[K]) => void,
    target:BasicTarget<Document>
): void



function useEventListener(eventName: string, handler: Function, target: Target) { }

Using function overloading , We're interested in the name of the event , Handle the parameters of the function , The goal is limited

  1. Event binding to the target
function useEventListener(eventName: string, handler: Function, target: Target{
   /*
      Every time the function refreshes , It will be carried out useEventListener, Means that the target repeatedly binds Events ,
       Use useRef You can guarantee that the event handler is up-to-date ,
       coordination useEffect You can guarantee that the target is bound to the event function only once
    */

    const handlerRef = useRef(handler)
    handlerRef.current = handler

    useEffect(() => {
        const targetElement = getTargetElement(target)!;

        if (!targetElement.addEventListener) {
            return
        }

        const eventListener = (e: Event): EventListenerOrEventListenerObject => {
            return handlerRef.current && handlerRef.current(e)
        }

        targetElement.addEventListener(eventName, eventListener)
      //  Remember to unbind , Avoid memory leaks
        return () => {
            targetElement.removeEventListener(eventName, eventListener)
        }

    }, [])
}
  1. Add options , Bubble or catch ? One time execution ? Whether to execute the default event ?
interface IOptions<T extends Target = Target> {
    target: T,
    once?: boolean,//  Whether to execute only once  false
    capture?: boolean,//  Whether to execute in the capture phase  false
    passive?: boolean //  Whether to execute the default event  false
}

function useEventListener<K extends keyof HTMLElementEventMap>(
    eventName: K,
    handler: (e: HTMLElementEventMap[K]) => void,
    options: IOptions<HTMLElement>
): void
;

function useEventListener<K extends keyof WindowEventMap>(
    eventName: K,
    handler: (e: WindowEventMap[K]) => void,
    options: IOptions<Window>
): void
;

function useEventListener<K extends keyof ElementEventMap>(
    eventName: K,
    handler: (e: ElementEventMap[K]) => void,
    options: IOptions<Element>
): void
;

function useEventListener<K extends keyof DocumentEventMap>(
    eventName: K,
    handler: (e: DocumentEventMap[K]) => void,
    options: IOptions<Document>
): void



function useEventListener(eventName: string, handler: Function, options: IOptions{
    const handlerRef = useRef(handler)
    handlerRef.current = handler

    useEffect(() => {
        const targetElement = getTargetElement(options.target, window)!

        if (!targetElement.addEventListener) {
            return
        }

      //AddEventListenerOptions  Added once and passive Two options
        const eventListener = (e: Event): EventListenerOrEventListenerObject | AddEventListenerOptions => {
            return handlerRef.current && handlerRef.current(e)
        }

        targetElement.addEventListener(eventName, eventListener, {
            once: options.once,
            passive: options.passive,
            capture: options.capture
        })

        return () => {
            targetElement.removeEventListener(eventName, eventListener, {
                capture: options.capture
            })
        }

    }, []
)
}

16. useEventTarget

Common form controls ( adopt e.target.value Get form values ) Of onChange Follow value Logic encapsulation , Support custom value conversion and reset function .

Examples of use :

import React, { Fragment } from 'react';
import { useEventTarget } from 'ahooks';

export default () => {
  const [value, { reset, onChange }] = useEventTarget({ initialValue'this is initial value' });
  return (
    <Fragment>
      <input value={value} onChange={onChange} style={{ width: 200marginRight: 20 }} />
      <button type="button" onClick={reset}>
        reset
      </button>
    </Fragment>

  );
};

Realization :

  1. Determine the input and output :

The main function is actually to get the values in the form , So the input can be initialValue:" The default value is " , And output , It has to be from the form value, And receiving the change of form value onChange function

type EventTarget<T>={
    target: {
        value:T
    }
}
export default function useTargetEvent<T>(initialValue: T): [T, (e: EventTarget<T>) => any];
  1. Realize the basic function
export default function useTargetEvent<T>(initialValue: T): [T, (e: EventTarget<T>) => any{
    const [value, setValue] = useState(initialValue)

    const onChange = useCallback((e: EventTarget<T>) => {
        setValue(e.target.value)
    }, [])
    
    return [
        value,
        onChange
    ]
}
  1. Realization reset function
export default function useTargetEvent<T>(initialValue: T{
    const [value, setValue] = useState(initialValue)

    //  Just reset to the initial value , therefore useCallback Dependency is an empty array
    const reset = useCallback(() => setValue(initialValue), [])
    const onChange = useCallback((e: EventTarget<T>) => {
        setValue(e.target.value)
    }, [])

    return [
        value,
        {
            onChange,
            reset
        }
    ]
}
  1. Realize the function of custom value conversion
type EventTarget<U> = {
    target: {
        value: U
    }
}
//  Using generics T  and  U, It's because I'm passing through transformer Before conversion ,
// target.value The type of is not necessarily related to initivalValue The same type
interface IOptions<T, U> {
    transformer: (e: U) => T,
    initialValue: T
}
export default function useTargetEvent<TU = T>(e: IOptions<T, U>{

    const { initialValue, transformer } = e

    const [value, setValue] = useState(initialValue)

    const transformerRef = useRef(transformer)
    transformerRef.current = transformer

    //  Just reset to the initial value , therefore useCallback Dependency is an empty array
    const reset = useCallback(() => setValue(initialValue), [])
    const onChange = useCallback((e: EventTarget<U>) => {

        const _value = e.target.value
    //  It's true transformer Whether there is
        if (typeof transformerRef.current === "function") {
            const value = transformerRef.current(_value)
            return setValue(value)
        }
        return setValue(_value as any as T)
    }, [])

    return [
        value,
        {
            onChange,
            reset
        }
    ] as const // as const TS It resolves to a constant , No, as const ,TS What you will return is (T|{...})[]
}

17.usePersistFn

In some scenes , You may need to use useCallback Remember a callback , But because internal functions have to be recreated often , The memory effect is not very good , Cause subcomponents to repeat render. For super complex sub components , Re rendering has an impact on performance . adopt usePersistFn, You can guarantee that the function address will never change .

  1. Use
type noop = (...args: any[]) => any;

const fn = usePersistFn<T extends noop>(
  fn: T,
);
  1. analysis
    1. Pass in usePersistFn(fn) Medium fn The address can change all the time , But the return function address remains unchanged
    2. Only use useRef You can get the latest functions , It doesn't cause function components to update ,

Realization :

type noop = (...args: any[]) => any
export default function usePersistFn<T extends noop>(fn: T): T {
    //  Use useRef Remember the function passed in from outside fn
    const fnRef = useRef(fn)
    fnRef.current = fn

    //  Use useRef Returns a function whose address does not change
    const persistFnRef = useRef<T>()
    if (!persistFnRef.current) {
        persistFnRef.current = function (thisany, ...args: any[]{

            return fnRef.current.apply(this, args)
        } as T
    }

    return persistFnRef.current
}

18. useScroll

Get the scrolling state of the element .

  1. Use
const position = useScroll(target, shouldUpdate);
// position = {top:number,left:number}
// target= HTMLElement | () => HTMLElement | Document |MutableRefObject
// shouldUpdate = ({ top: number, left: number}) => boolean

Realization  :

  1. Determine the input and output :
//  Call function useScroll And then it's back position
interface Position {
    left: number
    top: number
}

export type Target = BasicTarget<HTMLElement | Document>//  The target type of listening
export type ScrollListenController = (val: Position) => boolean // onScroll The controller , Returns a Boolean value to control whether to update position
function useScroll(target?: Target, shouldUpdate: ScrollListenController = () => true): Position
  1. Give the target dom binding scroll event
export default function useScroll(target?: Target, shouldUpdate: ScrollListenController = () => true{

    useEffect(() => {
        const el = getTargetElement(target, document)!
        if (!el.addEventListener) {
            return
        }

        function listener(event: Event{

        }
        el.addEventListener("scroll", listener)

        return () => {
            return el.removeEventListener("scroll", listener)
        }

    })//  Dependency is empty , Every time a component is refreshed, it needs to be redone dom binding scroll event
}
  1. to update position
export default function useScroll(target?: Target, shouldUpdate: ScrollListenController = () => true{
    const [position, setPosition] = useState<Position>({
        left: NaN,
        top: NaN
    })

    //  Persistence shouldUpdate
    const shouldUpdatePersistFn = usePersistFn(shouldUpdate)

    useEffect(() => {
        const el = getTargetElement(target, document)!
        if (!el.addEventListener) {
            return
        }

        function updatePosition(currentTarget: Target{
            let newPosition: Position;
            if (currentTarget === document) {
                if (!currentTarget.scrollingElement) {
                    return
                }
                //  The form scrolling elements on the desktop and mobile sides are different
                // document.documentElement.scrollTop;  The desktop
                // document.body.scrollTop;  Mobile
                //  In order to be compatible with mobile terminal and desktop terminal , have access to document.scrollingElement, Can automatically identify rolling containers on different platforms .
               // https://www.zhangxinxu.com/wordpress/2019/02/document-scrollingelement/
                newPosition = {
                    left: currentTarget.scrollingElement.scrollLeft,
                    top: currentTarget.scrollingElement.scrollTop
                }
            } else {
                newPosition = {
                    left: (currentTarget as HTMLElement).scrollLeft,
                    top: (currentTarget as HTMLElement).scrollTop
                }
            }
          //  return true To update position
            if (shouldUpdatePersistFn(position)) {
                setPosition(newPosition)
            }
        }
        //  On initialization , to update position
        updatePosition(el as Target)
      
        function listener(event: Event{
            if (!event.target) {
                return;
            }
            updatePosition(event.target as Target)

        }
        el.addEventListener("scroll", listener)

        return () => {
            return el.removeEventListener("scroll", listener)
        }


    })
  
  return position;
}
版权声明
本文为[Hu Zhiwu]所创,转载请带上原文链接,感谢
https://qdmana.com/2021/04/20210407203151074k.html

  1. VSLAM front end: image feature extraction
  2. Exclusive dialogue with the person in charge of Alibaba cloud function computing: what you don't know about serverless
  3. 「开源免费」基于Vue和Quasar的前端SPA项目crudapi后台管理系统实战之序列号自定义组件(四)
  4. "Open source and free" serial number customization component of crudapi background management system of front end spa project based on Vue and Quasar (4)
  5. JavaScript 相似度排序
  6. Springboot项目搭建(前端到数据库,超详细)
  7. Less than 150 lines of code to write a python version of the snake
  8. 02_Nginx部署服务
  9. vue 快速入门 系列 —— vue 的基础应用(上)
  10. JavaScript similarity ranking
  11. 基于Vue和Quasar的前端SPA项目crudapi后台管理系统实战之布局菜单嵌套路由(三)
  12. Springboot project construction (front end to database, super detailed)
  13. 02_ Nginx Deployment Services
  14. vue 快速入门 系列 —— vue 的基础应用(上)
  15. Vue quick start series basic application of Vue
  16. Layout menu nested routing of front end spa project crudapi background management system based on Vue and Quasar (3)
  17. Vue quick start series basic application of Vue
  18. 一个好用的Visual Studio Code扩展 - Live Server,适用于前端小工具开发
  19. 基于Vue和Quasar的前端SPA项目实战之用户登录(二)
  20. css常用选择器总结
  21. Behind the miracle of the sixth championship is the football with AI blessing in the Bundesliga
  22. An easy to use Visual Studio code extension - live server, suitable for front-end gadget development
  23. 用 Python 抓取公号文章保存成 HTML
  24. User login of front end spa project based on Vue and Quasar (2)
  25. Summary of common selectors in CSS
  26. Using Python to grab articles with public number and save them as HTML
  27. To "restless" you
  28. 【免费开源】基于Vue和Quasar的crudapi前端SPA项目实战—环境搭建 (一)
  29. 【微信小程序】引入阿里巴巴图标库iconfont
  30. layui表格点击排序按钮后,表格绑定事件失效解决方法
  31. Unity解析和显示/播放GIF图片,支持http url,支持本地file://,支持暂停、继续播放
  32. 【vue】 export、export default、import的用法和区别
  33. [free and open source] crudapi front end spa project based on Vue and Quasar
  34. [wechat applet] introduces Alibaba icon library iconfont
  35. Layui table click Sort button, table binding event failure solution
  36. Element树形控件Tree踩坑:修改current-node-key无效
  37. Unity parses and displays / plays GIF images, supports HTTP URL, supports local file: / /, supports pause and resume playback
  38. Element树形控件Tree踩坑:修改current-node-key无效
  39. The usage and difference of export, export default and import
  40. Element tree control: invalid to modify current node key
  41. Element tree control: invalid to modify current node key
  42. linux下安装apache(httpd-2.4.3版本)各种坑
  43. How to install Apache (httpd-2.4.3) under Linux
  44. 程序员业余时间写的代码也算公司的?Nginx之父被捕引发争议
  45. Nacos serialize for class [com.alibaba.nacos.common.http.HttpRestResult] failed.
  46. Do programmers write code in their spare time? Controversy over the arrest of nginx's father
  47. Nacos serialize for class [ com.alibaba.nacos . common.http.HttpRestResult ] failed.
  48. Seamless management of API documents using eolink and gitlab
  49. vue 的基础应用(上)
  50. 28岁开始零基础学前端,这些血的教训你一定要避免
  51. Basic application of Vue
  52. Starting at the age of 28, you must avoid these bloody lessons
  53. Ubuntu 16.04 can not connect to the wireless solution and QQ installation
  54. Industry security experts talk about the rapid development of digital economy, how to guarantee the security of data elements?
  55. 利用Vue实现一个简单的购物车功能
  56. Behind the "tireless classroom" and teacher training, can byte education really "work wonders"?
  57. Using Vue to realize a simple shopping cart function
  58. 【css】伪类和伪类元素的区别
  59. 【css效果】实现简单的下拉菜单
  60. 【vue】父子组件传值