misc-hooks v0.1.1
misc-hooks - Precious React Hooks Library
1. makeAtom(), useAtom(atom) A Simple State Management Hook
Sample Usage
// create atom
const atom = makeAtom<T>()
const atom = makeAtom<T>(initialValue) // with initial value
// use atom in React component
const value = useAtom(atom)
// getter and setter
atom.value = newValue // set value synchronously
value = atom.value // get value synchronously
// subscribe for changes
unsub = atom.sub(val => console.log(val))
unsub = atom.sub(() => console.log(atom.value))
unsub() // unsubscribeAPI
makeAtom(initialValue?: T): create an atom with an initial value.useAtom(atom: Atom<T>): T: use an atom in a React component.atom.value: get or set the value synchronously.
2. useAsyncEffect() Async Effect Hook And makeDisposer() Utility
Sample Usage
useAsyncEffect(async ({signal, addDispose}) => {
const loader = makeLoader()
signal.addEventListener('abort', () => loader.abort())
const value = await loader.loadData(params)
window.addEventListener('resize', value.update)
return () => {
window.removeEventListener('resize', value.update)
value.dispose()
}
}, [params])API
useAsyncEffect(effectFn) is a hook that is similar to useEffect(), but it can return a cleanup function asynchronously.
The effect function effectFn is called with an object {signal, addDispose}.
signal: anAbortSignalobject that is aborted when the component is unmounted or the effect is re-run.addDispose(dispose?: () => void): add a function to be called when the component is unmounted or the effect is re-run. If the component is unmounted or the effect is reloaded before,disposeis immediately and synchronously called.addDispose()returns a function to remove the added function.
makeDisposer(): is a utility function that returns an object with the following properties:
addDispose(fn: () => void): add a function to be called when thedispose()method is called. Ifdispose()method is called before,fnis immediately and synchronously called.addDispose()returns a function to remove the added function.signal: anAbortSignalobject that is aborted when thedispose()method is called.dispose(): abort the signal and call all functions added byaddDispose().
3. useAsync() Async Data Loading Hook
Sample Usage
const {data, error, reload, loading} = useAsync(async () => await fetchData(params))
useEffect(() => {reload().catch(() => {})}, [params, reload]) // load data in first render and when params changes
if (error) throw error // propagate error to ErrorBoundaryAPI
Signature: const {error, data, reload, loading} = useAsync<T>(asyncFn, getInitial).
(required)
asyncFn: (disposer) => Promise<T> | Tis a function that returns the data or a promise resolving the data.(optional)
getInitial?: () => T | undefined: a function that optionally returns initial data.
getInitial if provided, is called in the server render, and in the first client render.
If it throws an error, the error is caught and set to error.
disposer: an object with the following properties:addDispose(dispose: () => void): add a function to be called when the component is unmounted or the data is reloaded. If the component is unmounted or the data is reloaded before,disposeis immediately and synchronously called.addDispose()returns a function to remove the added function.signal: anAbortSignalobject that is aborted when the component is unmounted or the data is reloaded.
loading: a boolean that istruewhen the data is loading.reload: a function that takes no argument, reloads the data and returns the result of theasyncFn.reloadvalue never changes, it can be safely used in the second argument ofuseEffect. In subsequent renders,reloaduses the latest functionasyncFnpassed to the hook.
Practical Usage
Practice 1:
When reload() is called, error and data are set to undefined (via setState) before asyncFn is called.
There is a case that the last data needs to be kept while reloading, for example, when changing a page number, you want to show the current data until the next page is loaded,
Use useKeep hook.
Practice 2: If you want to delay showing the loading indicator, use useTimedOut hook.
Practice 3: If params is an object, and you want to reload the data when the object changes, use useDeepMemo hook.
Sample usage:
const memoParams = useDeepMemo(params)
const {data, error, reload, loading} = useAsync((disposer) => loadData(memoParams))
useEffect(() => {reload().catch(() => {})}, [memoParams, reload]) // load data when params deeply changes
const timedOut = useTimedOut(500)
const dataKeep = useKeep(data)
if (error) throw error
return dataKeep // has data
? <Data data={dataKeep}/>
: timedOut // loading
? <Loading/>
: null // show empty when loading is too fastSSR Guide:
useAsync() can be used in SSR by providing getInitial function.
getInitial is called in only in the server render, and in the first client render.
In server side, in
getInitial: check data availability via a store defined within the request scope.- If data is available, return the data synchronously.
- If data is not available:
- Return
undefinedsynchronously - Trigger data loading, retain the promise for later use.
- Mark the render not ready to return to the client.
- Wait for all data loaded.
- Re-render the component with the loaded data.
- Return
In client side:
- Store SSR data in the global scope.
- Start hydration with the SSR data.
- Clear the SSR data after the first render:
useEffect(() => clearSSRData(), []). - In
getInitial: check data availability via the SSR data stored globally.- If data is available, return the data synchronously.
- If data is not available: return
undefinedsynchronously.
To load data only when the data is not available in SSR:
Load only once: use useRef() to check if the data is already loaded.
const {data, reload} = useAsync(fetchData, () => getSSRData(deepParams))
const dataRef = useRef(data) // use useRef() to avoid re-render
dataRef.current = data
useEffect(() => void (!dataRef.current && reload().catch(() => {})) , [reload]) // reload never changes and is safe to place in the second argumentRe-load when params changes, client-rendering version without SSR support:
const deepParams = useDeepMemo(params)
const {data, reload} = useAsync(async () => await fetchData(deepParams), () => getSSRData(params))
useEffect(() => void(reload()), [deepBody, reload])Combine the two to support SSR, only load if data is empty and re-load when params changes: use useEffectWithPrevDeps():
const deepParams = useDeepMemo(params)
const {data, reload} = useAsync(async () => await fetchData(deepParams), () => getSSRData(params))
const dataRef = useRef(data) // or useRefValue(data) or useEffectEvent(data)
dataRef.current = data
useEffectWithPrevDeps(
([prevBody, prevReload]) => void ((prevReload || !dataRef.current) && reload().catch(() => {})),
[deepBody, reload]
)4. Atomic Action Hooks
Sample Usage
Sample useAtomicCallback:
const [loading, onSave] = useAtomicCallback(async () => await saveData(data))
return <button onClick={onSave} disabled={loading}>Save</button>Sample useAtomicMaker:
const [loading, makeAtomic] = useAtomicMaker()
return <>
<button onClick={makeAtomic(onSave)} disabled={loading}>Save</button>
<button onClick={makeAtomic(onDelete)} disabled={loading}>Delete</button>
</>Sample useAtomicMaker:
const [loading, makeAtomic] = useAtomicMaker()
return <>
<button onClick={onSave} disabled={loading}>Save</button>
<button onClick={onDelete} disabled={loading}>Delete</button>
</>
async function onSave() {
await makeAtomic(async () => await saveData(data))
}
async function onDelete() {
await makeAtomic(async () => await deleteData(data))
}API
[loading, atomicCb] = useAtomicCallback(cb): convert a callback function cb to an atomic callback function.
The atomic callback function is a function that can be called only once at a time.
If the atomic callback function is called when the previous one is running, the new one returns undefined.
The function returns an array [loading, atomicCb]:
loading: a boolean that istruewhen the atomic callback function is running.atomicCb: the atomic callback function.
[loading, makeAtomic] = useAtomicMaker(): the hook to create an atomic maker, used to combine multiple functions into atomic functions.
useAtomicMaker() takes no argument and returns an array [loading, makeAtomic]:
loading: a boolean that istruewhen the atomic function is running.makeAtomic(cb): a function to make the argument functioncbatomic.
5. Other Hooks
Frequently used hooks
useEffectWithPrevDeps((prevDeps) => {}, [...deps]): similar touseEffect, but provides previous deps to the effect function.memoValue = useDeepMemo(value): get a memoized value.valueis compared bydeep-equalpackage.lastDefinedValue = useKeep(value): keep the last defined value. Whenvalueisundefined, the last non-undefinedvalue is returned.ref = useRefValue(value): similar touseEffectEvent, get a ref whose value is always the latestvalue.timedout = useTimedOut(timeout): get a boolean whose value istrueaftertimeoutms.state = useDebounce(value, timeout): get a debounced value.stateis updated after at leasttimeoutms.[state, setState, stateRef] = useRefState(initialState): similar touseState.stateRef's value is set immediately and synchronously aftersetStateis called. Note:initialStatecan not be a function.update = useForceUpdate(): get a function to force re-render component.
Others
[state, setState] = useDefaultState(defaultState): whendefaultStatechanges, setstatetodefaultState.[state, update] = useUpdate(getValue): get a function to force re-render component.getValueis a function to get the latest value to compare with the previous value. The latestgetValueis always used (useReducerspecs).nextState = nextStateFromAction(action, state): get next state fromsetStateaction.[state, toggle] = useToggle(init = false):toggle()to toggle booleanstatestate, or,toggle(true/false)to set state.[state, enable] = useTurnOn():enable()to set state totrue.[state, disable] = useTurnOff():disable()to set state tofalse.unmountedRef = useUnmountedRef(): get a ref whose value istruewhen component is unmounted. Note, from react 18, the effect is sometimes unmounted and mounted again.mountedRef = useMountedRef(): get a ref whose value istruewhen component is mounted. Note: ref's value is not set tofalsewhen component is unmounted.mounted = useMounted(): get a boolean whose value istruewhen component is mounted. Note: the value is not set tofalsewhen component is unmounted.prefRef = usePrevRef(value): get a ref whose value is the previousvalue.useEffectOnce(() => {}, [...deps]): similar touseEffect, but fires only once.useLayoutEffectWithPrevDeps((prevDeps) => {}, [...deps]):useLayoutEffectversion ofuseEffectWithPrevDeps.[state, setState, stateRef] = useEnhancedState(initialState): similar touseState, but also returns a ref whose value is always the lateststate.{value, setValue} = usePropState(initialState): similar touseState, but the returned value is an object, not an array.scopeId = useScopeId(prefix?: string): get a function to generate scoped id.prefixis the prefix of the id. The id is generated byscopeId(name?: string) = prefix + id + name.idis a SSR-statically random number generated byuseId().Type
OptionalArray(type).useListData(): utility to load list data. Usage:
const {list, hasPrev, hasNext, loadPrev, loadNext} = useListData({
initial: {
list, // default list
hasNext, // default hasNext
hasPrev, // default hasPrev
},
async load({before, after}) { // function to load data
return {
records, // new records
hasMore, // whether there are more records
}
}
})1 year ago
1 year ago
12 months ago
1 year ago
12 months ago
11 months ago
2 years ago
2 years ago
2 years ago
1 year ago
1 year ago
1 year ago
1 year ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago