UNPKG

@hakuna-matata-ui/hooks

Version:
1,059 lines (914 loc) 28.9 kB
import * as React from 'react'; import React__default, { useState, useCallback, useEffect, useRef } from 'react'; import { isBrowser, runIfFn, getBox, callAllHandlers, wrapPointerEventHandler, getPointerEventName, hasFocusWithin, focus, getActiveElement, contains, isTabbable, detectBrowser, isRefObject, isActiveElement, getOwnerDocument, getAllFocusable, noop, PanSession } from '@hakuna-matata-ui/utils'; import copy from 'copy-to-clipboard'; /** * React hook to manage boolean (on - off) states * * @param initialState the initial boolean state value */ function useBoolean(initialState) { if (initialState === void 0) { initialState = false; } var _useState = useState(initialState), value = _useState[0], setValue = _useState[1]; var on = useCallback(function () { setValue(true); }, []); var off = useCallback(function () { setValue(false); }, []); var toggle = useCallback(function () { setValue(function (prev) { return !prev; }); }, []); return [value, { on: on, off: off, toggle: toggle }]; } /** * useSafeLayoutEffect enables us to safely call `useLayoutEffect` on the browser * (for SSR reasons) * * React currently throws a warning when using useLayoutEffect on the server. * To get around it, we can conditionally useEffect on the server (no-op) and * useLayoutEffect in the browser. * * @see https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85 */ var useSafeLayoutEffect = isBrowser ? React.useLayoutEffect : React.useEffect; /** * React hook to persist any value between renders, * but keeps it up-to-date if it changes. * * @param value the value or function to persist */ function useCallbackRef(fn, deps) { if (deps === void 0) { deps = []; } var ref = React.useRef(fn); useSafeLayoutEffect(function () { ref.current = fn; }); // eslint-disable-next-line react-hooks/exhaustive-deps return React.useCallback(function () { for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } return ref.current == null ? void 0 : ref.current.apply(ref, args); }, deps); } function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; } var _excluded = ["timeout"]; /** * React hook to copy content to clipboard * * @param text the text or value to copy * @param {Number} [optionsOrTimeout=1500] optionsOrTimeout - delay (in ms) to switch back to initial state once copied. * @param {Object} optionsOrTimeout * @param {string} optionsOrTimeout.format - set the desired MIME type * @param {number} optionsOrTimeout.timeout - delay (in ms) to switch back to initial state once copied. */ function useClipboard(text, optionsOrTimeout) { if (optionsOrTimeout === void 0) { optionsOrTimeout = {}; } var _useState = useState(false), hasCopied = _useState[0], setHasCopied = _useState[1]; var _ref = typeof optionsOrTimeout === "number" ? { timeout: optionsOrTimeout } : optionsOrTimeout, _ref$timeout = _ref.timeout, timeout = _ref$timeout === void 0 ? 1500 : _ref$timeout, copyOptions = _objectWithoutPropertiesLoose(_ref, _excluded); var onCopy = useCallback(function () { var didCopy = copy(text, copyOptions); setHasCopied(didCopy); }, [text, copyOptions]); useEffect(function () { var timeoutId = null; if (hasCopied) { timeoutId = window.setTimeout(function () { setHasCopied(false); }, timeout); } return function () { if (timeoutId) { window.clearTimeout(timeoutId); } }; }, [timeout, hasCopied]); return { value: text, onCopy: onCopy, hasCopied: hasCopied }; } /** * Creates a constant value over the lifecycle of a component. * * Even if `useMemo` is provided an empty array as its final argument, it doesn't offer * a guarantee that it won't re-run for performance reasons later on. By using `useConstant` * you can ensure that initialisers don't execute twice or more. */ function useConst(init) { var ref = useRef(null); if (ref.current === null) { ref.current = typeof init === "function" ? init() : init; } return ref.current; } function useControllableProp(prop, state) { var isControlled = prop !== undefined; var value = isControlled && typeof prop !== "undefined" ? prop : state; return [isControlled, value]; } /** * React hook for using controlling component state. * @param props */ function useControllableState(props) { var valueProp = props.value, defaultValue = props.defaultValue, onChange = props.onChange, _props$shouldUpdate = props.shouldUpdate, shouldUpdate = _props$shouldUpdate === void 0 ? function (prev, next) { return prev !== next; } : _props$shouldUpdate; var onChangeProp = useCallbackRef(onChange); var shouldUpdateProp = useCallbackRef(shouldUpdate); var _React$useState = React.useState(defaultValue), valueState = _React$useState[0], setValue = _React$useState[1]; var isControlled = valueProp !== undefined; var value = isControlled ? valueProp : valueState; var updateValue = React.useCallback(function (next) { var nextValue = runIfFn(next, value); if (!shouldUpdateProp(value, nextValue)) { return; } if (!isControlled) { setValue(nextValue); } onChangeProp(nextValue); }, [isControlled, onChangeProp, value, shouldUpdateProp]); return [value, updateValue]; } /** * Reack hook to measure a component's dimensions * * @param ref ref of the component to measure * @param observe if `true`, resize and scroll observers will be turned on */ function useDimensions(ref, observe) { var _React$useState = React.useState(null), dimensions = _React$useState[0], setDimensions = _React$useState[1]; var rafId = React.useRef(); useSafeLayoutEffect(function () { if (!ref.current) return undefined; var node = ref.current; function measure() { rafId.current = requestAnimationFrame(function () { var boxModel = getBox(node); setDimensions(boxModel); }); } measure(); if (observe) { window.addEventListener("resize", measure); window.addEventListener("scroll", measure); } return function () { if (observe) { window.removeEventListener("resize", measure); window.removeEventListener("scroll", measure); } if (rafId.current) { cancelAnimationFrame(rafId.current); } }; }, [observe]); return dimensions; } function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } // This implementation is heavily inspired by react-aria's implementation var defaultIdContext = { prefix: Math.round(Math.random() * 10000000000), current: 0 }; var IdContext = /*#__PURE__*/React.createContext(defaultIdContext); var IdProvider = /*#__PURE__*/React.memo(function (_ref) { var children = _ref.children; var currentContext = React.useContext(IdContext); var isRoot = currentContext === defaultIdContext; var context = React.useMemo(function () { return { prefix: isRoot ? 0 : ++currentContext.prefix, current: 0 }; }, [isRoot, currentContext]); return /*#__PURE__*/React.createElement(IdContext.Provider, { value: context }, children); }); function useId(idProp, prefix) { var context = React.useContext(IdContext); return React.useMemo(function () { return idProp || [prefix, context.prefix, ++context.current].filter(Boolean).join("-"); }, // eslint-disable-next-line react-hooks/exhaustive-deps [idProp, prefix]); } /** * Reack hook to generate ids for use in compound components * * @param idProp the external id passed from the user * @param prefixes array of prefixes to use * * @example * * ```js * const [buttonId, menuId] = useIds("52", "button", "menu") * * // buttonId will be `button-52` * // menuId will be `menu-52` * ``` */ function useIds(idProp) { for (var _len = arguments.length, prefixes = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { prefixes[_key - 1] = arguments[_key]; } var id = useId(idProp); return React.useMemo(function () { return prefixes.map(function (prefix) { return prefix + "-" + id; }); }, [id, prefixes]); } /** * Used to generate an id, and after render, check if that id is rendered so we know * if we can use it in places such as `aria-labelledby`. * * @param partId - The unique id for the component part * * @example * const { ref, id } = useOptionalPart<HTMLInputElement>(`${id}-label`) */ function useOptionalPart(partId) { var _React$useState = React.useState(null), id = _React$useState[0], setId = _React$useState[1]; var ref = React.useCallback(function (node) { setId(node ? partId : null); }, [partId]); return { ref: ref, id: id, isRendered: Boolean(id) }; } function useDisclosure(props) { if (props === void 0) { props = {}; } var _props = props, onCloseProp = _props.onClose, onOpenProp = _props.onOpen, isOpenProp = _props.isOpen, idProp = _props.id; var onOpenPropCallbackRef = useCallbackRef(onOpenProp); var onClosePropCallbackRef = useCallbackRef(onCloseProp); var _React$useState = React.useState(props.defaultIsOpen || false), isOpenState = _React$useState[0], setIsOpen = _React$useState[1]; var _useControllableProp = useControllableProp(isOpenProp, isOpenState), isControlled = _useControllableProp[0], isOpen = _useControllableProp[1]; var id = useId(idProp, "disclosure"); var onClose = React.useCallback(function () { if (!isControlled) { setIsOpen(false); } onClosePropCallbackRef == null ? void 0 : onClosePropCallbackRef(); }, [isControlled, onClosePropCallbackRef]); var onOpen = React.useCallback(function () { if (!isControlled) { setIsOpen(true); } onOpenPropCallbackRef == null ? void 0 : onOpenPropCallbackRef(); }, [isControlled, onOpenPropCallbackRef]); var onToggle = React.useCallback(function () { var action = isOpen ? onClose : onOpen; action(); }, [isOpen, onOpen, onClose]); return { isOpen: !!isOpen, onOpen: onOpen, onClose: onClose, onToggle: onToggle, isControlled: isControlled, getButtonProps: function getButtonProps(props) { if (props === void 0) { props = {}; } return _extends({}, props, { "aria-expanded": "true", "aria-controls": id, onClick: callAllHandlers(props.onClick, onToggle) }); }, getDisclosureProps: function getDisclosureProps(props) { if (props === void 0) { props = {}; } return _extends({}, props, { hidden: !isOpen, id: id }); } }; } /** * React hook for performant `useCallbacks` * * @see https://github.com/facebook/react/issues/14099#issuecomment-440013892 * * @deprecated Use `useCallbackRef` instead. `useEventCallback` will be removed * in a future version. */ function useEventCallback(callback) { var ref = React.useRef(callback); useSafeLayoutEffect(function () { ref.current = callback; }); return React.useCallback(function (event) { for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { args[_key - 1] = arguments[_key]; } return ref.current.apply(ref, [event].concat(args)); }, []); } /** * React hook to manage browser event listeners * * @param event the event name * @param handler the event handler function to execute * @param doc the dom environment to execute against (defaults to `document`) * @param options the event listener options * * @internal */ function useEventListener(event, handler, env, options) { var listener = useCallbackRef(handler); React.useEffect(function () { var _runIfFn; var node = (_runIfFn = runIfFn(env)) != null ? _runIfFn : document; node.addEventListener(event, listener, options); return function () { node.removeEventListener(event, listener, options); }; }, [event, env, options, listener]); return function () { var _runIfFn2; var node = (_runIfFn2 = runIfFn(env)) != null ? _runIfFn2 : document; node.removeEventListener(event, listener, options); }; } function useEventListenerMap() { var listeners = React.useRef(new Map()); var currentListeners = listeners.current; var add = React.useCallback(function (el, type, listener, options) { var pointerEventListener = wrapPointerEventHandler(listener, type === "pointerdown"); listeners.current.set(listener, { __listener: pointerEventListener, type: getPointerEventName(type), el: el, options: options }); el.addEventListener(type, pointerEventListener, options); }, []); var remove = React.useCallback(function (el, type, listener, options) { var _listeners$current$ge = listeners.current.get(listener), pointerEventListener = _listeners$current$ge.__listener; el.removeEventListener(type, pointerEventListener, options); listeners.current["delete"](pointerEventListener); }, []); React.useEffect(function () { return function () { currentListeners.forEach(function (value, key) { remove(value.el, value.type, key, value.options); }); }; }, [remove, currentListeners]); return { add: add, remove: remove }; } /** * React effect hook that invokes only on update. * It doesn't invoke on mount */ var useUpdateEffect = function useUpdateEffect(effect, deps) { var mounted = React.useRef(false); React.useEffect(function () { if (mounted.current) { return effect(); } mounted.current = true; return undefined; // eslint-disable-next-line react-hooks/exhaustive-deps }, deps); return mounted.current; }; /** * React hook to focus an element conditionally * * @param ref the ref of the element to focus * @param options focus management options */ function useFocusEffect(ref, options) { var shouldFocus = options.shouldFocus, preventScroll = options.preventScroll; useUpdateEffect(function () { var node = ref.current; if (!node || !shouldFocus) return; if (!hasFocusWithin(node)) { focus(node, { preventScroll: preventScroll, nextTick: true }); } }, [shouldFocus, ref, preventScroll]); } function preventReturnFocus(containerRef) { var el = containerRef.current; if (!el) return false; var activeElement = getActiveElement(el); if (!activeElement) return false; if (contains(el, activeElement)) return false; if (isTabbable(activeElement)) return true; return false; } /** * Popover hook to manage the focus when the popover closes or hides. * * We either want to return focus back to the popover trigger or * let focus proceed normally if user moved to another interactive * element in the viewport. */ function useFocusOnHide(containerRef, options) { var shouldFocusProp = options.shouldFocus, visible = options.visible, focusRef = options.focusRef; var shouldFocus = shouldFocusProp && !visible; useUpdateEffect(function () { if (!shouldFocus) return; if (preventReturnFocus(containerRef)) { return; } var el = (focusRef == null ? void 0 : focusRef.current) || containerRef.current; if (el) { focus(el, { nextTick: true }); } }, [shouldFocus, containerRef, focusRef]); } /** * Credit goes to `framer-motion` of this useful utilities. * License can be found here: https://github.com/framer/motion */ /** * @internal */ function usePointerEvent(env, eventName, handler, options) { return useEventListener(getPointerEventName(eventName), wrapPointerEventHandler(handler, eventName === "pointerdown"), env, options); } /** * Polyfill to get `relatedTarget` working correctly consistently * across all browsers. * * It ensures that elements receives focus on pointer down if * it's not the active active element. * * @internal */ function useFocusOnPointerDown(props) { var ref = props.ref, elements = props.elements, enabled = props.enabled; var isSafari = detectBrowser("Safari"); var doc = function doc() { return getOwnerDocument(ref.current); }; usePointerEvent(doc, "pointerdown", function (event) { if (!isSafari || !enabled) return; var target = event.target; var els = elements != null ? elements : [ref]; var isValidTarget = els.some(function (elementOrRef) { var el = isRefObject(elementOrRef) ? elementOrRef.current : elementOrRef; return contains(el, target); }); if (!isActiveElement(target) && isValidTarget) { event.preventDefault(); focus(target); } }); } var defaultOptions = { preventScroll: true, shouldFocus: false }; function useFocusOnShow(target, options) { if (options === void 0) { options = defaultOptions; } var _options = options, focusRef = _options.focusRef, preventScroll = _options.preventScroll, shouldFocus = _options.shouldFocus, visible = _options.visible; var element = isRefObject(target) ? target.current : target; var autoFocus = shouldFocus && visible; var onFocus = useCallback(function () { if (!element || !autoFocus) return; if (contains(element, document.activeElement)) return; if (focusRef != null && focusRef.current) { focus(focusRef.current, { preventScroll: preventScroll, nextTick: true }); } else { var tabbableEls = getAllFocusable(element); if (tabbableEls.length > 0) { focus(tabbableEls[0], { preventScroll: preventScroll, nextTick: true }); } } }, [autoFocus, preventScroll, element, focusRef]); useUpdateEffect(function () { onFocus(); }, [onFocus]); useEventListener("transitionend", onFocus, element); } function useUnmountEffect(fn, deps) { if (deps === void 0) { deps = []; } return React.useEffect(function () { return function () { return fn(); }; }, // eslint-disable-next-line react-hooks/exhaustive-deps deps); } function useForceUpdate() { var unloadingRef = React.useRef(false); var _React$useState = React.useState(0), count = _React$useState[0], setCount = _React$useState[1]; useUnmountEffect(function () { unloadingRef.current = true; }); return React.useCallback(function () { if (!unloadingRef.current) { setCount(count + 1); } }, [count]); } /** * React Hook that provides a declarative `setInterval` * * @param callback the callback to execute at interval * @param delay the `setInterval` delay (in ms) */ function useInterval(callback, delay) { var fn = useCallbackRef(callback); React.useEffect(function () { var intervalId = null; var tick = function tick() { return fn(); }; if (delay !== null) { intervalId = window.setInterval(tick, delay); } return function () { if (intervalId) { window.clearInterval(intervalId); } }; }, [delay, fn]); } /** * React hook to persist any value between renders, * but keeps it up-to-date if it changes. * * @param value the value or function to persist */ function useLatestRef(value) { var ref = React.useRef(null); ref.current = value; return ref; } /* eslint-disable react-hooks/exhaustive-deps */ function assignRef(ref, value) { if (ref == null) return; if (typeof ref === "function") { ref(value); return; } try { // @ts-ignore ref.current = value; } catch (error) { throw new Error("Cannot assign value '" + value + "' to ref '" + ref + "'"); } } /** * React hook that merges react refs into a single memoized function * * @example * import React from "react"; * import { useMergeRefs } from `@hakuna-matata-ui/hooks`; * * const Component = React.forwardRef((props, ref) => { * const internalRef = React.useRef(); * return <div {...props} ref={useMergeRefs(internalRef, ref)} />; * }); */ function useMergeRefs() { for (var _len = arguments.length, refs = new Array(_len), _key = 0; _key < _len; _key++) { refs[_key] = arguments[_key]; } return React.useMemo(function () { if (refs.every(function (ref) { return ref == null; })) { return null; } return function (node) { refs.forEach(function (ref) { if (ref) assignRef(ref, node); }); }; }, refs); } /** * @deprecated `useMouseDownRef` will be removed in a future version. */ function useMouseDownRef(shouldListen) { if (shouldListen === void 0) { shouldListen = true; } var mouseDownRef = React__default.useRef(); useEventListener("mousedown", function (event) { if (shouldListen) { mouseDownRef.current = event.target; } }); return mouseDownRef; } /** * Example, used in components like Dialogs and Popovers so they can close * when a user clicks outside them. */ function useOutsideClick(props) { var ref = props.ref, handler = props.handler, _props$enabled = props.enabled, enabled = _props$enabled === void 0 ? true : _props$enabled; var savedHandler = useCallbackRef(handler); var stateRef = useRef({ isPointerDown: false, ignoreEmulatedMouseEvents: false }); var state = stateRef.current; useEffect(function () { if (!enabled) return; var onPointerDown = function onPointerDown(e) { if (isValidEvent(e, ref)) { state.isPointerDown = true; } }; var onMouseUp = function onMouseUp(event) { if (state.ignoreEmulatedMouseEvents) { state.ignoreEmulatedMouseEvents = false; return; } if (state.isPointerDown && handler && isValidEvent(event, ref)) { state.isPointerDown = false; savedHandler(event); } }; var onTouchEnd = function onTouchEnd(event) { state.ignoreEmulatedMouseEvents = true; if (handler && state.isPointerDown && isValidEvent(event, ref)) { state.isPointerDown = false; savedHandler(event); } }; var doc = getOwnerDocument(ref.current); doc.addEventListener("mousedown", onPointerDown, true); doc.addEventListener("mouseup", onMouseUp, true); doc.addEventListener("touchstart", onPointerDown, true); doc.addEventListener("touchend", onTouchEnd, true); return function () { doc.removeEventListener("mousedown", onPointerDown, true); doc.removeEventListener("mouseup", onMouseUp, true); doc.removeEventListener("touchstart", onPointerDown, true); doc.removeEventListener("touchend", onTouchEnd, true); }; }, [handler, ref, savedHandler, state, enabled]); } function isValidEvent(event, ref) { var _ref$current; var target = event.target; if (event.button > 0) return false; // if the event target is no longer in the document if (target) { var doc = getOwnerDocument(target); if (!doc.body.contains(target)) return false; } return !((_ref$current = ref.current) != null && _ref$current.contains(target)); } function usePanGesture(ref, props) { var onPan = props.onPan, onPanStart = props.onPanStart, onPanEnd = props.onPanEnd, onPanSessionStart = props.onPanSessionStart, onPanSessionEnd = props.onPanSessionEnd, threshold = props.threshold; var hasPanEvents = Boolean(onPan || onPanStart || onPanEnd || onPanSessionStart || onPanSessionEnd); var panSession = useRef(null); var handlers = { onSessionStart: onPanSessionStart, onSessionEnd: onPanSessionEnd, onStart: onPanStart, onMove: onPan, onEnd: function onEnd(event, info) { panSession.current = null; onPanEnd == null ? void 0 : onPanEnd(event, info); } }; useEffect(function () { var _panSession$current; (_panSession$current = panSession.current) == null ? void 0 : _panSession$current.updateHandlers(handlers); }); function onPointerDown(event) { panSession.current = new PanSession(event, handlers, threshold); } usePointerEvent(function () { return ref.current; }, "pointerdown", hasPanEvents ? onPointerDown : noop); useUnmountEffect(function () { var _panSession$current2; (_panSession$current2 = panSession.current) == null ? void 0 : _panSession$current2.end(); panSession.current = null; }); } function usePrevious(value) { var ref = useRef(); useEffect(function () { ref.current = value; }, [value]); return ref.current; } /** * Checks if the key pressed is a printable character * and can be used for shortcut navigation */ function isPrintableCharacter(event) { var key = event.key; return key.length === 1 || key.length > 1 && /[^a-zA-Z0-9]/.test(key); } /** * React hook that provides an enhanced keydown handler, * that's used for key navigation within menus, select dropdowns. */ function useShortcut(props) { if (props === void 0) { props = {}; } var _props = props, _props$timeout = _props.timeout, timeout = _props$timeout === void 0 ? 300 : _props$timeout, _props$preventDefault = _props.preventDefault, preventDefault = _props$preventDefault === void 0 ? function () { return true; } : _props$preventDefault; var _React$useState = React.useState([]), keys = _React$useState[0], setKeys = _React$useState[1]; var timeoutRef = React.useRef(); var flush = function flush() { if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } }; var clearKeysAfterDelay = function clearKeysAfterDelay() { flush(); timeoutRef.current = setTimeout(function () { setKeys([]); timeoutRef.current = null; }, timeout); }; React.useEffect(function () { return flush; }, []); function onKeyDown(fn) { return function (event) { if (event.key === "Backspace") { var keysCopy = [].concat(keys); keysCopy.pop(); setKeys(keysCopy); return; } if (isPrintableCharacter(event)) { var _keysCopy = keys.concat(event.key); if (preventDefault(event)) { event.preventDefault(); event.stopPropagation(); } setKeys(_keysCopy); fn(_keysCopy.join("")); clearKeysAfterDelay(); } }; } return onKeyDown; } /** * React hook that provides a declarative `setTimeout` * * @param callback the callback to run after specified delay * @param delay the delay (in ms) */ function useTimeout(callback, delay) { var fn = useCallbackRef(callback); React.useEffect(function () { if (delay == null) return undefined; var timeoutId = null; timeoutId = window.setTimeout(function () { fn(); }, delay); return function () { if (timeoutId) { window.clearTimeout(timeoutId); } }; }, [delay, fn]); } function useWhyDidYouUpdate(name, props) { var previousProps = React.useRef(); React.useEffect(function () { if (previousProps.current) { var allKeys = Object.keys(_extends({}, previousProps.current, props)); var changesObj = {}; allKeys.forEach(function (key) { if (previousProps.current[key] !== props[key]) { changesObj[key] = { from: previousProps.current[key], to: props[key] }; } }); if (Object.keys(changesObj).length) { console.log("[why-did-you-update]", name, changesObj); } } previousProps.current = props; }); } export { IdProvider, assignRef, useBoolean, useCallbackRef, useClipboard, useConst, useControllableProp, useControllableState, useDimensions, useDisclosure, useEventCallback, useEventListener, useEventListenerMap, useFocusEffect, useFocusOnHide, useFocusOnPointerDown, useFocusOnShow, useForceUpdate, useId, useIds, useInterval, useLatestRef, useMergeRefs, useMouseDownRef, useOptionalPart, useOutsideClick, usePanGesture, usePointerEvent, usePrevious, useSafeLayoutEffect, useShortcut, useTimeout, useUnmountEffect, useUpdateEffect, useWhyDidYouUpdate };