facebook/react · error · Error

Unsupported node type: ${expression.type}

Error message

Unsupported node type: ${expression.type}

What it means

While building the property-chain strings it uses to track stable values, the exhaustive-deps rule walks each dependency expression with analyzePropertyChain. Identifiers and non-computed member/optional-member chains map to strings like 'a.b.c', but a ChainExpression that wraps a CallExpression (an optional call such as foo?.()) has no property-chain representation, so the analysis throws. In practice this means a dependency entry containing an optional function invocation crashes the rule.

Source

Thrown at packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts:1957

    const object = analyzePropertyChain(node.object, optionalChains);
    const property = analyzePropertyChain(node.property, null);
    const result = `${object}.${property}`;
    markNode(node, optionalChains, result);
    return result;
  } else if (node.type === 'OptionalMemberExpression' && !node.computed) {
    const object = analyzePropertyChain(node.object, optionalChains);
    const property = analyzePropertyChain(node.property, null);
    const result = `${object}.${property}`;
    markNode(node, optionalChains, result);
    return result;
  } else if (
    node.type === 'ChainExpression' &&
    (!('computed' in node) || !node.computed)
  ) {
    const expression = node.expression;

    if (expression.type === 'CallExpression') {
      throw new Error(`Unsupported node type: ${expression.type}`);
    }

    const object = analyzePropertyChain(expression.object, optionalChains);
    const property = analyzePropertyChain(expression.property, null);
    const result = `${object}.${property}`;
    markNode(expression, optionalChains, result);
    return result;
  } else {
    throw new Error(`Unsupported node type: ${node.type}`);
  }
}

function getNodeWithoutReactNamespace(
  node: Expression | Super,
): Expression | Identifier | Super {
  if (
    node.type === 'MemberExpression' &&
    node.object.type === 'Identifier' &&

View on GitHub (pinned to eafeac097b)

Solutions

  1. Update eslint-plugin-react-hooks - newer versions handle more optional-chain shapes without crashing
  2. Depend on the function reference itself and invoke it inside the callback: list props.getValue and call props.getValue?.() in the effect body
  3. Hoist the result to a variable before the hook if the value is already computed, and depend on that variable
  4. As a last resort, add // eslint-disable-next-line react-hooks/exhaustive-deps with a comment explaining why the expression cannot be listed

Example fix

// before
useEffect(() => {
  props.onReady?.();
}, [props.onReady?.()]);

// after
const onReady = props.onReady;
useEffect(() => {
  onReady?.();
}, [onReady]);
Defensive patterns

Strategy: validation

Validate before calling

// Pre-flight scan: flag optional calls in dependency arrays before lint runs
// (cheap grep-style check usable in CI or a pre-commit hook)
const optionalCallInDeps = /\[[^\]]*?\?\.[^\]]*?\(/.test(sourceOfHookFile);
if (optionalCallInDeps) {
  console.warn('Optional call inside a deps array - rewrite as fn reference + fn?.() in the callback');
}

Prevention

When it happens

Trigger: A dependency array entry that is an optional call: useEffect(() => {...}, [props.getValue?.()]), useMemo(..., [ref.current?.()]), or chains ending in a call like a?.b.c(). The ChainExpression branch accepts only member-style chains; the CallExpression inside it hits the explicit throw.

Common situations: Codebases using optional chaining heavily; TypeScript strict mode where a function prop is possibly undefined; refactoring that moved an optional invocation from the callback body into the deps array; older plugin versions before optional-chain handling matured.

Related errors


AI-assisted analysis of facebook/react@eafeac097b (2026-08-21). Data as JSON: /api/errors/e177bc3af8bd1887. Report an issue: GitHub.