Understand these 12 Hooks and make sure you can play React (4)

March 30, 2023 1352hotness 0likes 0comments

case


case 1: useReactive

useReactive : a useState with responsive

reason: we know that variables can be defined in the format

with useState

const [count, setCount] = useState<number>(0)

set it through setCount , and get it by count . Only in this way can the view be rendered

Let's take a look at the normal operation, like this let count = 0; count = 7 the value of count is 7, that is to say, the data is responsive

so can we also write useState as responsive ? I am free to set the value of count, and I can get the latest value of count at any time, instead of setting it through setCount .

Let's think about how to implement a useState with responsive characteristics, that is, useRective
If you are interested in the following questions, you can think for yourself first:

  • how to set the import and export parameters of this hook?
  • how to make the data responsive (after all, normal operations can't refresh the view)?
  • how do I use TS to write and refine its type?
  • how to optimize it better?

analyze

the key of the above four small questions is the second . How do we make the data responsive ? if we want to make it responsive, we must monitor the change of the value and make changes, that is to say, when we operate on this number, we need to intercept intercept . Then we need a knowledge point of ES6 : Proxy

the points of Proxy and Reflect will be used here

Proxy : the accepted parameter is object , so the first problem is solved, and the input parameter is the object. So how to refresh the view? Here, use the useUpdate above to force the refresh to make the data change.

as for optimization, use useCreation mentioned above, and then use useRef to play initialState .

Code

import { useRef } from 'react';
import { useUpdate, useCreation } from '../index';

const observer = <T extends Record<string, any>>(initialVal: T, cb: () => void): T => {

 const proxy = new Proxy<T>(initialVal, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver);
      return typeof res === 'object' ? observer(res, cb) : Reflect.get(target, key);
    },
    set(target, key, val) {
      const ret = Reflect.set(target, key, val);
      cb();
      return ret;
    },
  });

  return proxy;
}

const useReactive = <T extends Record<string, any>>(initialState: T):T => {
  const ref = useRef<T>(initialState);
  const update = useUpdate();

  const state = useCreation(() => {
    return observer(ref.current, () => {
      update();
    });
  }, []);

  return state
};

export default useReactive;

Let's start with TS , because we don't know what type of initialState will be passed, so we need to use generic here. The parameter we accept is object , which is in the form of key-value, where key is string,value can be any type, so we use Record

next intercept this , we just need to intercept set (set) and get (get) , where:

  • set this block, you need to change the graph, that is to say, use useUpdate to force refresh
  • .

  • to get this piece, you need to determine whether it is an object. If so, continue recursion. If not, return
  • .

verify

next let's verify the useReactive we wrote. We will verify it in terms of strings, numbers, Boolean, arrays, functions, and computed properties:

    import { Button } from 'antd-mobile';
    import React from 'react';
    import { useReactive } from '@/components'

    const Index:React.FC<any> = (props)=> {

      const state = useReactive<any>({
        count: 0,
        name: 'star',
        flag: true,
        arr: [],
        bugs: ['star', 'react', 'hook'],
        addBug(bug:string) {
          this.bugs.push(bug);
        },
        get bugsCount() {
          return this.bugs.length;
        },
      })

      return (
        <div style={{padding: 20}}>
          <div style={{fontWeight: 'bold'}}>Basic usage:</div>
           <div style={{marginTop: 8}}> Manipulating numbers: {state.count}</div>
           <div style={{margin: '8px 0', display: 'flex',justifyContent: 'flex-start'}}>
             <Button color='primary' onClick={() => state.count++ } >Plus 1</Button>
             <Button color='primary' style={{marginLeft: 8}} onClick={() => state.count-- } >Minus 1</Button>
             <Button color='primary' style={{marginLeft: 8}} onClick={() => state.count = 7 } >Set to 7</Button>
           </div>
           <div style={{marginTop: 8}}> Manipulate strings:{state.name}</div>
           <div style={{margin: '8px 0', display: 'flex',justifyContent: 'flex-start'}}>
             <Button color='primary' onClick={() => state.name = 'star' } >Set to star</Button>
             <Button color='primary' style={{marginLeft: 8}} onClick={() => state.name = 'Domesy'} >Set to Domesy</Button>
           </div>
           <div style={{marginTop: 8}}> Manipulate Boolean values:{JSON.stringify(state.flag)}</div>
           <div style={{margin: '8px 0', display: 'flex',justifyContent: 'flex-start'}}>
             <Button color='primary' onClick={() => state.flag = !state.flag } >Switch state</Button>
           </div>
           <div style={{marginTop: 8}}> Manipulating an array: {JSON.stringify(state.arr)}</div>
           <div style={{margin: '8px 0', display: 'flex',justifyContent: 'flex-start'}}>
             <Button color="primary" onClick={() => state.arr.push(Math.floor(Math.random() * 100))} >push</Button>
             <Button color="primary" style={{marginLeft: 8}} onClick={() => state.arr.pop()} >pop</Button>
             <Button color="primary" style={{marginLeft: 8}} onClick={() => state.arr.shift()} >shift</Button>
             <Button color="primary" style={{marginLeft: 8}} onClick={() => state.arr.unshift(Math.floor(Math.random() * 100))} >unshift</Button>
             <Button color="primary" style={{marginLeft: 8}} onClick={() => state.arr.reverse()} >reverse</Button>
             <Button color="primary" style={{marginLeft: 8}} onClick={() => state.arr.sort()} >sort</Button>
           </div>
           <div style={{fontWeight: 'bold', marginTop: 8}}>Calculation Properties:</div>
           <div style={{marginTop: 8}}>number:{ state.bugsCount } individual</div>
           <div style={{margin: '8px 0'}}>
             <form
               onSubmit={(e) => {
                 state.bug ? state.addBug(state.bug) : state.addBug('domesy')
                 state.bug = '';
                 e.preventDefault();
               }}
             >
               <input type="text" value={state.bug} onChange={(e) => (state.bug = e.target.value)} />
               <button type="submit"  style={{marginLeft: 8}} >Add</button>
               <Button color="primary" style={{marginLeft: 8}} onClick={() => state.bugs.pop()}>Delete</Button>
             </form>

           </div>
           <ul>
             {
               state.bugs.map((bug:any, index:number) => (
                 <li key={index}>{bug}</li>
               ))
             }
           </ul>
        </div>
      );
    }

    export default Index;

case 2: useEventListener

reason: we need to monitor all kinds of events, such as click events, keyboard events, scrolling events, etc. We encapsulate them to facilitate subsequent calls

to put it bluntly, it is encapsulated on the basis of addEventListener . Let's first think about what we need on this basis.

first of all, the input parameters of useEventListener can be divided into three

  • the first event is an event (e.g. click, keydown)
  • second callback function (so no parameter is required)
  • the third is the target (whether it is a node or a global)

one thing to note here is that when destroys, you need to remove the corresponding listening event

Code

    import { useEffect } from 'react';

    const useEventListener = (event: string, handler: (...e:any) => void, target: any = window) => {

      useEffect(() => {
        const targetElement  = 'current' in target ? target.current : window;
        const useEventListener = (event: Event) => {
          return handler(event)
        }
        targetElement.addEventListener(event, useEventListener)
        return () => {
          targetElement.removeEventListener(event, useEventListener)
        }
      }, [event])
    };

    export default useEventListener;

Note: here target is set to window by default, but why do you write this: 'current' in target because all the values we get with useRef are ref.current

support SSR (optimization)

useEffectWithTarget is used in the original ahooks code, but it is mistaken that this is similar to the optimization effect of useCreation , but it is not. The purpose of doing this is to support SSR

because the type of SSR is () = & gt; HTMLElement , if this is taken as a parameter of useEffect , then it means that deps does not exist, that is, when other variables change, useEffect will be executed. Therefore, in order to fully support the dynamic change of target , this useEffectWithTarget will be born.

detailed code

    import { useEffect } from 'react';
    import type { DependencyList } from 'react';
    import { useRef } from 'react';
    import useLatest from '../useLatest';
    import useUnmount from '../useUnmount';

    const depsAreSame = (oldDeps: DependencyList, deps: DependencyList):boolean => {
      for(let i = 0; i < oldDeps.length; i++) {
        if(!Object.is(oldDeps[i], deps[i])) return false
      }
      return true
    }

    const useEffectTarget = (effect: () => void, deps:DependencyList, target: any) => {

      const hasInitRef = useRef(false); // Initial setup initialization
      const elementRef = useRef<(Element | null)[]>([]);// Store specific values
      const depsRef = useRef<DependencyList>([]); // Store passed deps
      const unmountRef = useRef<any>(); // Store the corresponding effect

      // Initialization and update of initialization components will be executed
      useEffect(() => {
        const targetElement  = 'current' in target ? target.current : window;

        // First assignment
        if(!hasInitRef.current){
          hasInitRef.current = true;

          elementRef.current = targetElement;
          depsRef.current = deps;
          unmountRef.current = effect();
          return
        }
        // Verification variable value: The dependent value changes due to different target values
        if(elementRef.current !== targetElement || !depsAreSame(deps, depsRef.current)){
          //Execute the corresponding function first
          unmountRef.current?.();
          //Reassign
          elementRef.current = targetElement;
          depsRef.current = deps; 
          unmountRef.current = effect();
        }
      })

      useUnmount(() => {
        unmountRef.current?.();
        hasInitRef.current = false;
      })
    }

    const useEventListener = (event: string, handler: (...e:any) => void, target: any = window) => {
      const handlerRef = useLatest(handler);

      useEffectTarget(() => {
        const targetElement  = 'current' in target ? target.current : window;

        //  Prevent the absence of the attribute addEventListener
        if(!targetElement?.addEventListener) return;

        const useEventListener = (event: Event) => {
          return handlerRef.current(event)
        }
        targetElement.addEventListener(event, useEventListener)
        return () => {
          targetElement.removeEventListener(event, useEventListener)
        }
      }, [event], target)
    };

    export default useEventListener;
  • only useEffect is used here because
  • is required for both update and initialization.

  • you must avoid the absence of the addEventListener attribute, and the listening target may not be loaded

verify

verify whether useEventListener can be used normally, and verify the initialization and uninstallation. Code:

    import React, { useState, useRef } from 'react';
    import { useEventListener } from '@/components'
    import { Button } from 'antd-mobile';

    const Index:React.FC<any> = (props)=> {

      const [count, setCount] = useState<number>(0)
      const [flag, setFlag] = useState<boolean>(true)
      const [key, setKey] = useState<string>('')
      const ref = useRef(null);

      useEventListener('click', () => setCount(v => v +1), ref)
      useEventListener('keydown', (ev) => setKey(ev.key));

      return (
        <div style={{padding: 20}}>
          <Button color='primary' onClick={() => {setFlag(v => !v)}}>changing-over {flag ? 'unmount' : 'mount'}</Button>
          {
            flag && <div>
              <div>Number:{count}</div>
              <button ref={ref} >Plus 1</button>
              <div>Listening for keyboard events:{key}</div>
            </div>
          }

        </div>
      );
    }

    export default Index;

We can use useEventListener to encapsulate other hooks, such as mouse hover, long press event, mouse position, etc. Here we give a small example of mouse hover

small example useHover

useHover : listen for DOM elements for rollover

this is very simple. You only need to listen to mouseenter and mouseleave via useEventListener , and return Boolean values:

    import { useState } from 'react';
    import useEventListener  from '../useEventListener';

    interface Options {
      onEnter?: () => void;
      onLeave?: () => void;
    }

    const useHover = (target:any, options?:Options): boolean => {

      const [flag, setFlag] = useState<boolean>(false)
      const { onEnter, onLeave } = options || {};

      useEventListener('mouseenter', () => {
        onEnter?.()
        setFlag(true)
      }, target)

      useEventListener('mouseleave', () => {
        onLeave?.()
        setFlag(false)
      }, target)

      return flag
    };

    export default useHover;
InterServer Web Hosting and VPS

Aaron

Hello, my name is Aaron and I am a freelance front-end developer

Comments