import React, { forwardRef, FunctionComponent, memo } from 'react' import { useStateTracking } from './useStateTracking' /** * Proxy handlers object used to intercept function calls to React components. * This enables automatic signal tracking by wrapping component execution * in reactive tracking context. * * The proxy intercepts the function call (apply trap) and wraps it with * useStateTracking to enable automatic dependency tracking for signals * accessed during render. * * @example * ```ts * // Used internally by track() function * const ProxiedComponent = new Proxy(MyComponent, ProxyHandlers) * ``` * * @internal */ export const ProxyHandlers = { /** * This is a function call trap for functional components. When this is called, we know it means * React did run 'Component()', that means we can use any hooks here to setup our effect and * store. * * With the native Proxy, all other calls such as access/setting to/of properties will be * forwarded to the target Component, so we don't need to copy the Component's own or inherited * properties. * * @see https://github.com/facebook/react/blob/2d80a0cd690bb5650b6c8a6c079a87b5dc42bd15/packages/react-reconciler/src/ReactFiberHooks.old.js#L460 */ apply(Component: FunctionComponent, thisArg: any, argumentsList: any) { // eslint-disable-next-line react-hooks/rules-of-hooks return useStateTracking(Component.displayName ?? Component.name ?? 'tracked(???)', () => Component.apply(thisArg, argumentsList) ) }, } /** * React internal symbol for identifying memoized components. * Used to detect if a component is already wrapped with React.memo(). * * @example * ```ts * const isMemoComponent = component['$$typeof'] === ReactMemoSymbol * ``` * * @internal */ export const ReactMemoSymbol = Symbol.for('react.memo') /** * React internal symbol for identifying forward ref components. * Used to detect if a component is wrapped with React.forwardRef(). * * @example * ```ts * const isForwardRefComponent = component['$$typeof'] === ReactForwardRefSymbol * ``` * * @internal */ export const ReactForwardRefSymbol = Symbol.for('react.forward_ref') /** * Returns a tracked version of the given component. * Any signals whose values are read while the component renders will be tracked. * If any of the tracked signals change later it will cause the component to re-render. * * This also wraps the component in a React.memo() call, so it will only re-render * when props change OR when any tracked signals change. This provides optimal * performance by preventing unnecessary re-renders while maintaining reactivity. * * The function handles special React component types like forwardRef and memo * components automatically, preserving their behavior while adding reactivity. * * @param baseComponent - The React functional component to make reactive to signal changes * @returns A memoized component that re-renders when props or tracked signals change * * @example * ```ts * import { atom } from '@tldraw/state' * import { track, useAtom } from '@tldraw/state-react' * * const Counter = track(function Counter(props: CounterProps) { * const count = useAtom('count', 0) * const increment = useCallback(() => count.set(count.get() + 1), [count]) * return * }) * * // Component automatically re-renders when count signal changes * ``` * * @example * ```ts * // Works with forwardRef components * const TrackedInput = track(React.forwardRef( * function TrackedInput(props, ref) { * const theme = useValue(themeSignal) * return * } * )) * ``` * * @public */ export function track>( baseComponent: T ): React.NamedExoticComponent> { let compare = null const $$typeof = baseComponent['$$typeof' as keyof typeof baseComponent] if ($$typeof === ReactMemoSymbol) { baseComponent = (baseComponent as any).type compare = (baseComponent as any).compare } if ($$typeof === ReactForwardRefSymbol) { return memo(forwardRef(new Proxy((baseComponent as any).render, ProxyHandlers) as any)) as any } return memo(new Proxy(baseComponent, ProxyHandlers) as any, compare) as any }