import { isDev } from '@alloc/is-dev' import * as React from 'react' import { forwardRef, ReactElement, Ref, RefAttributes, useMemo } from 'react' import { useLayoutEffect } from 'react-layout-effect' import { Auto, AutoObserver } from '../auto' import { batch } from '../batch' import { addDebugAction, getDebug, setDebug } from '../debug' import { globals } from '../globals' import { AutoDepth, useAutoDepth } from './context' import { RenderAction, useDispose, useForceUpdate } from './common' interface Component

{ (props: P): ReactElement | null displayName?: string } interface RefForwardingComponent { (props: P, ref: Ref): ReactElement | null displayName?: string } type RefForwardingAuto = T & (( props: T extends RefForwardingComponent ? P & RefAttributes : never ) => ReactElement | null) /** Wrap a component with magic observable tracking */ export function withAuto(render: T): T /** Wrap a component with `forwardRef` and magic observable tracking */ export function withAuto( render: T ): RefForwardingAuto /** @internal */ export function withAuto(render: any) { let component: React.FunctionComponent = (props, ref) => { const { auto, depth, commit } = useAutoRender(component) // Subscribe to observables as early as possible, because // we don't want effects to trigger the previous observer. useLayoutEffect(() => commit(observer, nonce)) // Track which observable state is used during render. const observer = auto.start(render) try { var content = render(props, ref) } finally { auto.stop() } // Cache the nonce for cancellation purposes. const { nonce } = observer // React might discard this render without telling us, but we can // detect that with our first child's render phase. const recon = ( { auto.nonce = nonce }} /> ) return ( {recon} {content} ) } if (render.length > 1) { // Bind its component name to the ref forwarder. Object.defineProperty(component, 'displayName', { get: () => component.displayName, }) component = forwardRef(component as any) } if (/^[A-Z]/.test(render.name)) { component.displayName = render.name } if (isDev) { Object.defineProperty(component, '__render', { value: render, }) } return component } // withAuto.dev is used by @wana/babel-plugin-with-auto if (isDev) { withAuto.dev = (render: any) => { const Component = render.length > 1 ? React.forwardRef(render) : render const AutoRender = React.forwardRef((props: any, ref: any) => { const { auto, depth, commit } = useAutoRender(Component) function useCommit(observer: AutoObserver) { let nonce = 0 useLayoutEffect(() => commit(observer, nonce)) return () => { nonce = observer.nonce return ( { auto.nonce = nonce }} /> ) } } return ( ) }) Object.defineProperty(AutoRender, 'displayName', { get: () => 'AutoRender', set(displayName) { Component.displayName = displayName }, }) return AutoRender } } function useAutoRender(component: React.FunctionComponent) { const depth = useAutoDepth() const forceUpdate = useForceUpdate() const auto = useMemo(() => { const auto = new Auto({ onDirty() { if (isDev) { addDebugAction(auto, 'dirty') } const { observer } = auto // If no observer exists, our `component` is either unmounted // or its first render is not committed yet. In the latter case, // the reconciled nonce will soon be validated in a layout effect. if (!observer) { return } // The `dirty` flag is reset when our `component` rerenders, // which means this `onDirty` function can be called again, // before this batched update is ever processed. batch.render(depth, () => { // When the observer changes before the batch can be flushed, // we bail out to avoid using the nonce of a value no longer observed. if (observer == auto.observer) { if (observer.nonce > auto.nonce) { if (isDev) { addDebugAction(auto, 'batch') } forceUpdate() } else { auto.dirty = false } } }) }, }) if (isDev) { setDebug(auto, { name: component.displayName || 'Anonymous', actions: ['init'], renders: 0, }) } return auto }, []) if (isDev) { getDebug(auto).renders!++ if (globals.onRender) { globals.onRender(auto, depth, component) } } useDispose(() => auto.dispose()) return { auto, depth, commit(observer: AutoObserver, nonce: number) { if (isDev) { getDebug(auto).actions = [] } // The `nonce` from the render phase will force an update // if any observed values have changed since then. if (!auto.commit(observer, nonce)) { if (isDev) { addDebugAction(auto, 'dirty') } forceUpdate() } else if (isDev) { addDebugAction(auto, 'observe') } }, } }