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 - you must avoid the absence of the
addEventListener
attribute, and the listening target may not be loaded
is required for both update and initialization.
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;