UNPKG

valuelink

Version:

Purely functional two-way data binding and form validation for React

164 lines (135 loc) 5.53 kB
import { useEffect, useRef, useState } from 'react'; import { helpers } from './helpers'; import { Link as StateRef, RefsHash } from './link'; export class UseStateRef<T> extends StateRef<T> { // Set the component's state value. set( x : T | ( ( x : T ) => T ) ) : void {} update( fun : ( x : T, event? : Object ) => T, event? : Object ) : void { // update function must be overriden to use state set // ability to delay an update, and to preserve link.update semantic. this.set( x => { const value = helpers( x ).clone( x ), result = fun( value, event ); return result === void 0 ? x : result; }); } constructor( value : T, set : ( x : T | ( ( x : T ) => T ) ) => void ){ super( value ); this.set = set; } } /** * Create the ref to the local state. */ export function useLink<S>( initialState : S | (() => S) ){ const [ value, set ] = useState( initialState ); return new UseStateRef( value, set ); } export { useLink as useStateRef, useSafeLink as useSafeStateRef, useBoundLink as useBoundStateRef, useSafeBoundLink as useSafeBoundStateRef } /** * Create the link to the local state which is safe to set when component is unmounted. * Use this for the state which is set when async I/O is completed. */ export function useSafeLink<S>( initialState : S | (() => S) ){ const [ value, set ] = useState( initialState ), isMounted = useIsMountedRef(); return new UseStateRef( value, x => isMounted.current && set( x ) ); } /** * Returns the ref which is true when component it mounted. */ export function useIsMountedRef(){ const isMounted = useRef( true ); useEffect( () => ( () => isMounted.current = false ), []); return isMounted; } /** * Create the link to the local state which is bound to another * value or link in a single direction. When the source changes, the link changes too. */ export function useBoundLink<T>( source : T | StateRef<T>) : StateRef<T> { const value = source instanceof StateRef ? source.value : source, link = useLink( value ); useEffect(() => link.set( value ), [ value ]); link.action return link; } /** * Create the safe link to the local state which is synchronized with another * value or link in a single direction. * When the source change, the linked state changes too. */ export function useSafeBoundLink<T>( source : T | StateRef<T> ) : StateRef<T> { const value = source instanceof StateRef ? source.value : source, link = useSafeLink( value ); useEffect(() => link.set( value ), [ value ]); return link; } /** * Persists links in local storage under the given key. * Links will be loaded on component's mount, and saved on unmount. * @param key - string key for the localStorage entry. * @param state - links to persist wrapped in an object `{ lnk1, lnk2, ... }` */ export function useLocalStorage( key : string, state : RefsHash ){ // save state to use on unmount... const stateRef = useRef<RefsHash>(); stateRef.current = state; useEffect(()=>{ const savedData = JSON.parse( localStorage.getItem( key ) || '{}' ); StateRef.setValues( stateRef.current, savedData ); return () =>{ const dataToSave = StateRef.getValues( stateRef.current ); localStorage.setItem( key, JSON.stringify( dataToSave ) ); } },[]); } /** * Wait for the promise (or async function) completion. * Execute operation once when mounted, returning `null` while the operation is pending. * When operation is completed, returns "ok" or "fail" depending on the result and * forces the local component update. * * const isReady = useIO( async () => { * const data = await fetchData(); * link.set( data ); * }); */ export function useIO( fun : () => Promise<any>, condition : any[] = [] ) : boolean { // Counter of open I/O requests. If it's 0, I/O is completed. // Counter is needed to handle the situation when the next request // is issued before the previous one was completed. const $isReady = useSafeLink<number>( null ); useEffect(()=>{ // function in set instead of value to avoid race conditions with counter increment. $isReady.set( x => ( x || 0 ) + 1 ); fun().finally(() => $isReady.set( x => x - 1 )); }, condition); // null is used to detect the first render when no requests issued yet // but the I/O is not completed. return $isReady.value === null ? false : !$isReady.value; } // Return an array of values to be used in useEffect hook. export function whenChanged( ...objs : any[] ) : any[]; export function whenChanged( a, b, c, d ) : any[] { const { length } = arguments; switch( length ){ case 1: return [ extractChangeToken( a ) ]; case 2: return [ extractChangeToken( a ), extractChangeToken( b ) ]; case 3: return [ extractChangeToken( a ), extractChangeToken( b ), extractChangeToken( c ) ]; default: const array = [ extractChangeToken( a ), extractChangeToken( b ), extractChangeToken( c ), extractChangeToken( d ) ]; for( let i = 4; i < length; i++ ){ array.push( extractChangeToken( arguments[ i ] ) ); } return array; } } function extractChangeToken( x : any ){ return x && x._changeToken !== void 0 ? x._changeToken : x; }