UNPKG

@kwiz/fluentui

Version:
174 lines 7.25 kB
import { makeStyles } from "@fluentui/react-components"; import { CommonLogger, isFunction, isNotEmptyArray, isNullOrEmptyString, isNullOrUndefined, isPrimitiveValue, jsonClone, jsonStringify, LoggerLevel, objectsEqual } from "@kwiz/common"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { mixins } from "../styles/styles"; /** Empty array ensures that effect is only run on mount */ export const useEffectOnlyOnMount = []; function extractStringValue(e) { try { if (e instanceof HTMLElement) return e.outerHTML; } catch (e) { } try { let json = jsonStringify(e); if (json === "{}") return Object.keys(e).join(); //maybe just object with functions, no members or values else return json; } catch (e) { } try { return e.toString(); } catch (e) { } return ''; } /** set state on steroids. provide promise callback after render, onChange transformer and automatic skip-set when value not changed */ export function useStateEX(initialValue, options) { options = options || {}; const name = options.name || ''; let logger = useMemo(() => new CommonLogger(`useStateWithTrack${isNullOrEmptyString(name) ? '' : ` ${name}`}`), [name]); logger.i.setLevel(LoggerLevel.WARN); const [value, setValueInState] = useState(initialValue); //you can't flip those options between renders const needToTrackChanges = useMemo(() => options.skipUpdateIfSame || isFunction(options.onChange), []); const currentValue = useRef(initialValue); //must set initial value don't rely on useEffect to do it since some dependents might try to access it on first load //json clone complex/ref values so we can compare if value changed, in case caller makes chagnes on the value object directly. const currentValueForChecks = useRef(); useEffect(() => { updateCurrentRef(initialValue); }, useEffectOnlyOnMount); //keep a ref to onChange so the caller's latet state is accessible const onChange = useRef(options.onChange); //keep it in sync onChange.current = options.onChange; /** make this a collection in case several callers are awaiting the same propr update */ const resolveState = useRef([]); const isMounted = useRef(false); useEffect(() => { isMounted.current = true; return () => { isMounted.current = false; }; }, useEffectOnlyOnMount); const resolvePromises = useCallback(() => { if (isNotEmptyArray(resolveState.current)) { let resolvers = resolveState.current.slice(); resolveState.current = []; //clear resolvers.map(r => r(currentValue.current)); } }, []); useEffect(() => { resolvePromises(); }, [value]); const getIsValueChanged = useCallback((newValue) => { let error = null; let result; try { if (!objectsEqual(newValue, currentValueForChecks.current)) { result = true; } else { result = false; } } catch (e) { error = e; result = true; } return logger.i.groupSync(result ? 'value changed' : 'value not changed', log => { if (logger.i.getLevel() === LoggerLevel.VERBOSE) { log('old: ' + extractStringValue(currentValueForChecks.current)); log('new: ' + extractStringValue(newValue)); if (error) log({ label: "Error", value: error }); } return result; }); }, []); function updateCurrentRef(newValue) { currentValue.current = newValue; //always update this ref- it is being used by callers, its fast and not expensive //this json clone on html elements for useRefWithState was killing our app //we don't need to clone currentValueForChecks if we don't have to if (!needToTrackChanges) return; currentValueForChecks.current = isPrimitiveValue(newValue) || isFunction(newValue) ? newValue //fix skipUpdateIfSame for complex objects //if we don't clone it, currentValue.current will be a ref to the value in the owner //and will be treated as unchanged object, and it will be out of sync //this leads to skipUpdateIfSame failing after just 1 unchanged update : jsonClone(newValue); } const setValue = useCallback((newState) => new Promise(resolve => { if (!isMounted.current) { //unmounted may never resolve logger.i.log(`resolved without wait`); resolve(newState); } else { resolveState.current.push(resolve); const isChanged = needToTrackChanges ? getIsValueChanged(newState) //don't call this if there is no onChange handler and if we don't need to monitor skipUpdateIfSame : true; if (isFunction(onChange.current)) newState = onChange.current(newState, isChanged); //keep current value ref up to date updateCurrentRef(newState); //set state if (!options.skipUpdateIfSame || isChanged) setValueInState(newState); else //don't set in state - just resolve pending promises, UI will not be updated. resolvePromises(); } }), useEffectOnlyOnMount); return useMemo(() => [value, setValue, currentValue], [value, setValue, currentValue]); } /** use a ref, that can be tracked as useEffect dependency */ export function useRefWithState(initialValue, stateOptions = { skipUpdateIfSame: true, name: "useRefWithState" }, /** if used in a control that also needs a forwardRef, set this to keep them in sync */ forwardRef) { let asRef = useRef(initialValue); let [asState, setState] = useStateEX(initialValue, stateOptions); //incorrect to use useCallback with no dependencies, broke licensing vertical tab list function setRef(newValue) { asRef.current = newValue; setState(newValue); } ; useEffect(() => { //setting the forwardRef if (!isNullOrUndefined(forwardRef)) { if (isFunction(forwardRef)) forwardRef(asRef.current); else forwardRef.current = asRef.current; } }, [asState]); return useMemo(() => ({ /** ref object for getting latest value in handlers */ ref: asRef, /** for useEffect dependency */ value: asState, /** for setting on element: ref={e.set} */ set: setRef }), [asRef, asState, setRef]); } const useStyles = makeStyles({ clickable: mixins.clickable, }); /** return props to make div appear as clickable, and accept enter key as click */ export function useClickableDiv() { const cssNames = useStyles(); const props = { className: cssNames.clickable, tabIndex: 0, onKeyDown: e => { if (e.key === "Enter") e.target.click(); } }; return props; } //# sourceMappingURL=hooks.js.map