Skip to content

Instantly share code, notes, and snippets.

@composite
Last active April 14, 2023 13:35
Show Gist options
  • Save composite/2ec11abe33d4a6e6041e998433fe218e to your computer and use it in GitHub Desktop.
Save composite/2ec11abe33d4a6e6041e998433fe218e to your computer and use it in GitHub Desktop.
Just React hook utilities.
import {
type ComponentProps,
createElement,
type ElementType,
type ForwardedRef,
forwardRef,
type MutableRefObject,
type ReactDOM,
type ReactElement,
type Ref,
type RefCallback,
type RefObject
} from "react";
export type ValidComponent = Parameters<typeof createElement>[0];
export type AsProp<C extends ValidComponent> = { as?: C };
export type RefProp<C extends ValidComponent> = { ref?: DynamicForwardedRef<C> };
export type DynamicComponent<T extends ValidComponent> = T extends keyof ReactDOM ? ElementType<T> : T;
export type DynamicBaseProps<C extends ValidComponent> = (C extends ReactElement<infer P> ? P : C extends keyof ReactDOM ? ComponentProps<C> : Record<string, any>)
export type DynamicProps<C extends ValidComponent, P = DynamicBaseProps<C>> = { [K in keyof P]: P[K] } & AsProp<C>
export type DynamicForwardedRef<T extends ValidComponent> = ForwardedRef<DynamicComponent<T>>
export type DynamicRef<T extends ValidComponent> = Ref<DynamicComponent<T>>
export type DynamicMutableRefObject<T extends ValidComponent> = MutableRefObject<DynamicComponent<T>>
export type DynamicRefCallback<T extends ValidComponent> = RefCallback<DynamicComponent<T>>
export type DynamicRefObject<T extends ValidComponent> = RefObject<DynamicComponent<T>>
const Dynamic = <T extends ValidComponent>({ as, children, ...others }: DynamicProps<T>, ref: DynamicForwardedRef<T>) => createElement(as ?? 'div', { ...others, ref } as any, children)
/**
* The react dynamic component solution. similar to Vue's `<Component>` and solid's `<Dynamic>`.
* Usage:
* ```jsx
* <Dynamic as="input" type="text" />
* <Dynamic as={YourComponent} yourProp={foo}>Your child content</Dynamic>
* ```
* @param props Desired component's props. even children, ref included.
* @param props.as (required) A string tagname for plain HTML element or Component object or function for your component.
* @return Your desired component.
*/
export default forwardRef(Dynamic) as <T extends ValidComponent = 'div'>(
props: DynamicProps<T> & RefProp<T>
) => ReturnType<typeof Dynamic>
/**
* The utility for assign `ref`.
* Usage:
* ```jsx
* <div ref={div => assignRef(myRef, div)}>div content</div>
* ```
* @param ref A ref callback or object.
* @param to A ref target for binding.
* @return assigned `ref` will returned.
*/
export const assignRef = <T, R extends Ref<T>>(ref: R, to: T): R => {
// const isFunction = o => o instanceof Function || typeof o === 'function'
isFunction(ref) ? ref(to) : ref && ((ref as MutableRefObject<T>).current = to)
return ref
}
/**
* `ref` for multiple `ref`s.
* Usage:
* ```jsx
* <div ref={mergeRef(ref1, ref2)}>div content</div>
* ```
* @param refs The `ref`s to binding.
* @return The `RefCallback` to bind.
*/
export const mergeRef = <T,R extends Ref<T>>(...refs: R[]): RefCallback<T> => (ref: T) => refs.forEach(r => assignRef(r, ref))
/**
* A Ref callback for normal or lazy behavior DOM effect.
* Usage: (same as `useRef`)
* ```jsx
* const myEvent = () => alert('hello!')
* const myRef = useRefCallback(
* node => node.addEventListener('click', myEvent),
* node => node.removeEventListener('click', myEvent)
* )
*
* <div ref={myRef}>div content</div>
* ```
* @param init callback when ref bound.
* @param dispose callback when unbound ref.
* @return A `RefCallback` with `.current` for access `ref` object anytime.
*/
export const useRefCallback = <T, R extends Ref<T>>(init: (ref: T) => void, dispose?: (ref: T) => void): RefCallback<T> & RefObject<T> => {
const ref = useRef<T>()
const callback = useCallback((node: T) => {
ref.current && dispose && dispose(ref.current)
node && init && init(node)
ref.current = node!
}, [])
return Object.assign(callback, { get current() { return ref.current } }) as RefCallback<T> & RefObject<T>
}
/**
* Hook setter to tracked by `Promise`.
* Usage:
* ```js
* const [state, setState] = useState(0)
* const setStatePromise = useSetStatePromise(setState, [state])
*
* const handleSubmit = async (e: Event) => {
* setState(1)
* const reponse1 = await fetch('/path/to/server', { method: 'POST', body: `state=${state}` }) // unchanged state '0' will provided.
* await setStatePromise(2)
* const reponse2 = await fetch('/path/to/server', { method: 'POST', body: `state=${state}` }) // changed state '2' will provided.
* }
* ```
* @param setter The setter to hook
* @param deps track to getters. `[]` will not tracked.
* @return The void promise that wait until state changed.
*/
export const useSetStatePromise = <T>(setter: Dispatch<SetStateAction<T>>, deps: DependencyList) => {
const ref = useRef<(a?: unknown) => void>()
useEffect(() => {
ref.current?.()
}, deps)
return (o: SetStateAction<T>): Promise<void> => {
setter(o)
return new Promise(ok => ref.current = ok).then(() => void (ref.current = undefined))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment