packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetShared.js JAVASCRIPT 666 lines View on github.com → Search inside
1/* eslint-disable dot-notation */23// Shared implementation and constants between the inline script and external4// runtime instruction sets.56const ELEMENT_NODE = 1;7const COMMENT_NODE = 8;8const ACTIVITY_START_DATA = '&';9const ACTIVITY_END_DATA = '/&';10const SUSPENSE_START_DATA = '$';11const SUSPENSE_END_DATA = '/$';12const SUSPENSE_PENDING_START_DATA = '$?';13const SUSPENSE_QUEUED_START_DATA = '$~';14const SUSPENSE_FALLBACK_START_DATA = '$!';1516const FALLBACK_THROTTLE_MS = 300;1718const SUSPENSEY_FONT_AND_IMAGE_TIMEOUT = 500;1920// If you have a target goal in mind for a metric to hit, you don't want the21// only reason you miss it by a little bit to be throttling heuristics.22// This tries to avoid throttling if avoiding it would let you hit this metric.23// This is derived from trying to hit an LCP of 2.5 seconds with some head room.24const TARGET_VANITY_METRIC = 2300;2526// TODO: Symbols that are referenced outside this module use dynamic accessor27// notation instead of dot notation to prevent Closure's advanced compilation28// mode from renaming. We could use extern files instead, but I couldn't get it29// working. Closure converts it to a dot access anyway, though, so it's not an30// urgent issue.3132export function revealCompletedBoundaries(batch) {33  window['$RT'] = performance.now();34  for (let i = 0; i < batch.length; i += 2) {35    const suspenseIdNode = batch[i];36    const contentNode = batch[i + 1];37    if (contentNode.parentNode === null) {38      // If the client has failed hydration we may have already deleted the streaming39      // segments. The server may also have emitted a complete instruction but cancelled40      // the segment. Regardless we can ignore this case.41    } else {42      // We can detach the content now.43      // Completions of boundaries within this contentNode will now find the boundary44      // in its designated place.45      contentNode.parentNode.removeChild(contentNode);46    }47    // Clear all the existing children. This is complicated because48    // there can be embedded Suspense boundaries in the fallback.49    // This is similar to clearSuspenseBoundary in ReactFiberConfigDOM.50    // TODO: We could avoid this if we never emitted suspense boundaries in fallback trees.51    // They never hydrate anyway. However, currently we support incrementally loading the fallback.52    const parentInstance = suspenseIdNode.parentNode;53    if (!parentInstance) {54      // We may have client-rendered this boundary already. Skip it.55      continue;56    }5758    // Find the boundary around the fallback. This is always the previous node.59    const suspenseNode = suspenseIdNode.previousSibling;6061    let node = suspenseIdNode;62    let depth = 0;63    do {64      if (node && node.nodeType === COMMENT_NODE) {65        const data = node.data;66        if (data === SUSPENSE_END_DATA || data === ACTIVITY_END_DATA) {67          if (depth === 0) {68            break;69          } else {70            depth--;71          }72        } else if (73          data === SUSPENSE_START_DATA ||74          data === SUSPENSE_PENDING_START_DATA ||75          data === SUSPENSE_QUEUED_START_DATA ||76          data === SUSPENSE_FALLBACK_START_DATA ||77          data === ACTIVITY_START_DATA78        ) {79          depth++;80        }81      }8283      const nextNode = node.nextSibling;84      parentInstance.removeChild(node);85      node = nextNode;86    } while (node);8788    const endOfBoundary = node;8990    // Insert all the children from the contentNode between the start and end of suspense boundary.91    while (contentNode.firstChild) {92      parentInstance.insertBefore(contentNode.firstChild, endOfBoundary);93    }9495    suspenseNode.data = SUSPENSE_START_DATA;96    if (suspenseNode['_reactRetry']) {97      requestAnimationFrame(suspenseNode['_reactRetry']);98    }99  }100  batch.length = 0;101}102103export function revealCompletedBoundariesWithViewTransitions(104  revealBoundaries,105  batch,106) {107  let shouldStartViewTransition = false;108  let autoNameIdx = 0;109  const restoreQueue = [];110  function applyViewTransitionName(element, classAttributeName) {111    const className = element.getAttribute(classAttributeName);112    if (!className) {113      return;114    }115    // Add any elements we apply a name to a queue to be reverted when we start.116    const elementStyle = element.style;117    restoreQueue.push(118      element,119      elementStyle['viewTransitionName'],120      elementStyle['viewTransitionClass'],121    );122    if (className !== 'auto') {123      elementStyle['viewTransitionClass'] = className;124    }125    let name = element.getAttribute('vt-name');126    if (!name) {127      // Auto-generate a name for this one.128      // TODO: We don't have a prefix to pick from here but maybe we don't need it129      // since it's only applicable temporarily during this specific animation.130      const idPrefix = '';131      name = '_' + idPrefix + 'T_' + autoNameIdx++ + '_';132    }133    // If the name isn't valid CSS identifier, base64 encode the name instead.134    // This doesn't let you select it in custom CSS selectors but it does work in current135    // browsers.136    const escapedName =137      CSS.escape(name) !== name ? 'r-' + btoa(name).replace(/=/g, '') : name;138    elementStyle['viewTransitionName'] = escapedName;139    shouldStartViewTransition = true;140  }141  try {142    const existingTransition = document['__reactViewTransition'];143    if (existingTransition) {144      // Retry after the previous ViewTransition finishes.145      existingTransition.finished.finally(window['$RV'].bind(null, batch));146      return;147    }148    // First collect all entering names that might form pairs exiting names.149    const appearingViewTransitions = new Map();150    for (let i = 1; i < batch.length; i += 2) {151      const contentNode = batch[i];152      const appearingElements = contentNode.querySelectorAll('[vt-share]');153      for (let j = 0; j < appearingElements.length; j++) {154        const appearingElement = appearingElements[j];155        appearingViewTransitions.set(156          appearingElement.getAttribute('vt-name'),157          appearingElement,158        );159      }160    }161    const suspenseyImages = [];162    // Next we'll find the nodes that we're going to animate and apply names to them..163    for (let i = 0; i < batch.length; i += 2) {164      const suspenseIdNode = batch[i];165      const parentInstance = suspenseIdNode.parentNode;166      if (!parentInstance) {167        // We may have client-rendered this boundary already. Skip it.168        continue;169      }170      const parentRect = parentInstance.getBoundingClientRect();171      if (172        !parentRect.left &&173        !parentRect.top &&174        !parentRect.width &&175        !parentRect.height176      ) {177        // If the parent instance is display: none then we don't animate this boundary.178        // This can happen when this boundary is actually a child of a different boundary that179        // isn't yet revealed or is about to be revealed, but in that case that boundary180        // should do the exit/enter and not this one. Conveniently this also lets us skip181        // this if it's just in a hidden tree in general.182        // TODO: Should we skip it if it's out of viewport? It's possible that it gets183        // brought into the viewport by changing size.184        // TODO: There's a another case where an inner boundary is inside a fallback that185        // is about to be deleted. In that case we should not run exit animations on the inner.186        continue;187      }188189      // Apply exit animations to the immediate elements inside the fallback.190      let node = suspenseIdNode;191      let depth = 0;192      while (node) {193        if (node.nodeType === COMMENT_NODE) {194          const data = node.data;195          if (data === SUSPENSE_END_DATA) {196            if (depth === 0) {197              break;198            } else {199              depth--;200            }201          } else if (202            data === SUSPENSE_START_DATA ||203            data === SUSPENSE_PENDING_START_DATA ||204            data === SUSPENSE_QUEUED_START_DATA ||205            data === SUSPENSE_FALLBACK_START_DATA206          ) {207            depth++;208          }209        } else if (node.nodeType === ELEMENT_NODE) {210          const exitElement = node;211          const exitName = exitElement.getAttribute('vt-name');212          const pairedElement = appearingViewTransitions.get(exitName);213          applyViewTransitionName(214            exitElement,215            pairedElement ? 'vt-share' : 'vt-exit',216          );217          if (pairedElement) {218            // Activate the other side as well.219            applyViewTransitionName(pairedElement, 'vt-share');220            appearingViewTransitions.set(exitName, null); // mark claimed221          }222          // Next we'll look inside this element for pairs to trigger "share".223          const disappearingElements =224            exitElement.querySelectorAll('[vt-share]');225          for (let j = 0; j < disappearingElements.length; j++) {226            const disappearingElement = disappearingElements[j];227            const name = disappearingElement.getAttribute('vt-name');228            const appearingElement = appearingViewTransitions.get(name);229            if (appearingElement) {230              applyViewTransitionName(disappearingElement, 'vt-share');231              applyViewTransitionName(appearingElement, 'vt-share');232              appearingViewTransitions.set(name, null); // mark claimed233            }234          }235          // Relay the exit to nested ViewTransitions that opted in236          const relayExitElements =237            exitElement.querySelectorAll('[vt-parent-exit]');238          for (let j = 0; j < relayExitElements.length; j++) {239            applyViewTransitionName(relayExitElements[j], 'vt-parent-exit');240          }241        }242        node = node.nextSibling;243      }244245      // Apply enter animations to the new nodes about to be inserted.246      const contentNode = batch[i + 1];247      let enterElement = contentNode.firstElementChild;248      while (enterElement) {249        const paired =250          appearingViewTransitions.get(enterElement.getAttribute('vt-name')) ===251          null;252        if (!paired) {253          applyViewTransitionName(enterElement, 'vt-enter');254        }255        // Relay the enter to nested ViewTransitions that opted in256        const relayEnterElements =257          enterElement.querySelectorAll('[vt-parent-enter]');258        for (let j = 0; j < relayEnterElements.length; j++) {259          applyViewTransitionName(relayEnterElements[j], 'vt-parent-enter');260        }261        enterElement = enterElement.nextElementSibling;262      }263264      // Apply update animations to any parents and siblings that might be affected.265      let ancestorElement = parentInstance;266      do {267        let childElement = ancestorElement.firstElementChild;268        while (childElement) {269          // TODO: Bail out if we can270          const updateClassName = childElement.getAttribute('vt-update');271          if (272            updateClassName &&273            updateClassName !== 'none' &&274            !restoreQueue.includes(childElement)275          ) {276            // If we have already handled this element as part of another exit/enter/share, don't override.277            applyViewTransitionName(childElement, 'vt-update');278          }279          childElement = childElement.nextElementSibling;280        }281      } while (282        (ancestorElement = ancestorElement.parentNode) &&283        ancestorElement.nodeType === ELEMENT_NODE &&284        ancestorElement.getAttribute('vt-update') !== 'none'285      );286287      // Find the appearing Suspensey Images inside the new content.288      const appearingImages = contentNode.querySelectorAll(289        'img[src]:not([loading="lazy"])',290      );291      // TODO: Consider marking shouldStartViewTransition if we found any images.292      // But only once we can disable the root animation for that case.293      suspenseyImages.push.apply(suspenseyImages, appearingImages);294    }295    if (shouldStartViewTransition) {296      const transition = (document['__reactViewTransition'] = document[297        'startViewTransition'298      ]({299        update: () => {300          revealBoundaries(batch);301          const blockingPromises = [302            // Force layout to trigger font loading, we stash the actual value to trick minifiers.303            document.documentElement.clientHeight,304            // Block on fonts finishing loading before revealing these boundaries.305            document.fonts.ready,306          ];307          for (let i = 0; i < suspenseyImages.length; i++) {308            const suspenseyImage = suspenseyImages[i];309            if (!suspenseyImage.complete) {310              const rect = suspenseyImage.getBoundingClientRect();311              const inViewport =312                rect.bottom > 0 &&313                rect.right > 0 &&314                rect.top < window.innerHeight &&315                rect.left < window.innerWidth;316              if (inViewport) {317                // TODO: Use decode() instead of the load event here once the fix in318                // https://issues.chromium.org/issues/420748301 has propagated fully.319                const loadingImage = new Promise(resolve => {320                  suspenseyImage.addEventListener('load', resolve);321                  suspenseyImage.addEventListener('error', resolve);322                });323                blockingPromises.push(loadingImage);324              }325            }326          }327          return Promise.race([328            Promise.all(blockingPromises),329            new Promise(resolve => {330              const currentTime = performance.now();331              const msUntilTimeout =332                // If the throttle would make us miss the target metric, then shorten the throttle.333                // performance.now()'s zero value is assumed to be the start time of the metric.334                currentTime < TARGET_VANITY_METRIC &&335                currentTime > TARGET_VANITY_METRIC - FALLBACK_THROTTLE_MS336                  ? TARGET_VANITY_METRIC - currentTime337                  : // Otherwise it's throttled starting from last commit time.338                    SUSPENSEY_FONT_AND_IMAGE_TIMEOUT;339              setTimeout(resolve, msUntilTimeout);340            }),341          ]);342        },343        types: [], // TODO: Add a hard coded type for Suspense reveals.344      }));345      transition.ready.finally(() => {346        // Restore all the names/classes that we applied to what they were before.347        // We do it in reverse order in case there were duplicates so the first one wins.348        for (let i = restoreQueue.length - 3; i >= 0; i -= 3) {349          const element = restoreQueue[i];350          const elementStyle = element.style;351          const previousName = restoreQueue[i + 1];352          elementStyle['viewTransitionName'] = previousName;353          const previousClassName = restoreQueue[i + 1];354          elementStyle['viewTransitionClass'] = previousClassName;355          if (element.getAttribute('style') === '') {356            element.removeAttribute('style');357          }358        }359      });360      transition.finished.finally(() => {361        if (document['__reactViewTransition'] === transition) {362          document['__reactViewTransition'] = null;363        }364      });365      // Queue any future completions into its own batch since they won't have been366      // snapshotted by this one.367      window['$RB'] = [];368      return;369    }370    // Fall through to reveal.371  } catch (x) {372    // Fall through to reveal.373  }374  // ViewTransitions v2 not supported or no ViewTransitions found. Reveal immediately.375  revealBoundaries(batch);376}377378export function clientRenderBoundary(379  suspenseBoundaryID,380  errorDigest,381  errorMsg,382  errorStack,383  errorComponentStack,384) {385  // Find the fallback's first element.386  const suspenseIdNode = document.getElementById(suspenseBoundaryID);387  if (!suspenseIdNode) {388    // The user must have already navigated away from this tree.389    // E.g. because the parent was hydrated.390    return;391  }392  // Find the boundary around the fallback. This is always the previous node.393  const suspenseNode = suspenseIdNode.previousSibling;394  // Tag it to be client rendered.395  suspenseNode.data = SUSPENSE_FALLBACK_START_DATA;396  // assign error metadata to first sibling397  const dataset = suspenseIdNode.dataset;398  if (errorDigest != null) dataset['dgst'] = errorDigest;399  if (errorMsg) dataset['msg'] = errorMsg;400  if (errorStack) dataset['stck'] = errorStack;401  if (errorComponentStack) dataset['cstck'] = errorComponentStack;402  // Tell React to retry it if the parent already hydrated.403  if (suspenseNode['_reactRetry']) {404    suspenseNode['_reactRetry']();405  }406}407408export function completeBoundary(suspenseBoundaryID, contentID) {409  const contentNodeOuter = document.getElementById(contentID);410  if (!contentNodeOuter) {411    // If the client has failed hydration we may have already deleted the streaming412    // segments. The server may also have emitted a complete instruction but cancelled413    // the segment. Regardless we can ignore this case.414    return;415  }416417  // Find the fallback's first element.418  const suspenseIdNodeOuter = document.getElementById(suspenseBoundaryID);419  if (!suspenseIdNodeOuter) {420    // We'll never reveal this boundary so we can remove its content immediately.421    // Otherwise we'll leave it in until we reveal it.422    // This is important in case this specific boundary contains other boundaries423    // that may get completed before we reveal this one.424    contentNodeOuter.parentNode.removeChild(contentNodeOuter);425426    // The user must have already navigated away from this tree.427    // E.g. because the parent was hydrated. That's fine there's nothing to do428    // but we have to make sure that we already deleted the container node.429    return;430  }431432  // Mark this Suspense boundary as queued so we know not to client render it433  // at the end of document load.434  const suspenseNodeOuter = suspenseIdNodeOuter.previousSibling;435  suspenseNodeOuter.data = SUSPENSE_QUEUED_START_DATA;436  // Queue this boundary for the next batch437  window['$RB'].push(suspenseIdNodeOuter, contentNodeOuter);438439  if (window['$RB'].length === 2) {440    // This is the first time we've pushed to the batch. We need to schedule a callback441    // to flush the batch. This is delayed by the throttle heuristic.442    if (typeof window['$RT'] !== 'number') {443      // If we haven't had our rAF callback yet, schedule everything for the first paint.444      requestAnimationFrame(window['$RV'].bind(null, window['$RB']));445    } else {446      const currentTime = performance.now();447      const msUntilTimeout =448        // If the throttle would make us miss the target metric, then shorten the throttle.449        // performance.now()'s zero value is assumed to be the start time of the metric.450        currentTime < TARGET_VANITY_METRIC &&451        currentTime > TARGET_VANITY_METRIC - FALLBACK_THROTTLE_MS452          ? TARGET_VANITY_METRIC - currentTime453          : // Otherwise it's throttled starting from last commit time.454            window['$RT'] + FALLBACK_THROTTLE_MS - currentTime;455      // We always schedule the flush in a timer even if it's very low or negative to allow456      // for multiple completeBoundary calls that are already queued to have a chance to457      // make the batch.458      setTimeout(window['$RV'].bind(null, window['$RB']), msUntilTimeout);459    }460  }461}462463export function completeBoundaryWithStyles(464  suspenseBoundaryID,465  contentID,466  stylesheetDescriptors,467) {468  const precedences = new Map();469  const thisDocument = document;470  let lastResource, node;471472  // Seed the precedence list with existing resources and collect hoistable style tags473  const nodes = thisDocument.querySelectorAll(474    'link[data-precedence],style[data-precedence]',475  );476  const styleTagsToHoist = [];477  for (let i = 0; (node = nodes[i++]); ) {478    if (node.getAttribute('media') === 'not all') {479      styleTagsToHoist.push(node);480    } else {481      if (node.tagName === 'LINK') {482        window['$RM'].set(node.getAttribute('href'), node);483      }484      precedences.set(node.dataset['precedence'], (lastResource = node));485    }486  }487488  let i = 0;489  const dependencies = [];490  let href, precedence, attr, loadingState, resourceEl, media;491492  function cleanupWith(cb) {493    this['_p'] = null;494    cb();495  }496497  // Sheets Mode498  let sheetMode = true;499  while (true) {500    if (sheetMode) {501      // Sheet Mode iterates over the stylesheet arguments and constructs them if new or checks them for502      // dependency if they already existed503      const stylesheetDescriptor = stylesheetDescriptors[i++];504      if (!stylesheetDescriptor) {505        // enter <style> Mode506        sheetMode = false;507        i = 0;508        continue;509      }510511      let avoidInsert = false;512      let j = 0;513      href = stylesheetDescriptor[j++];514515      if ((resourceEl = window['$RM'].get(href))) {516        // We have an already inserted stylesheet.517        loadingState = resourceEl['_p'];518        avoidInsert = true;519      } else {520        // We haven't already processed this href so we need to construct a stylesheet and hoist it521        // We construct it here and attach a loadingState. We also check whether it matches522        // media before we include it in the dependency array.523        resourceEl = thisDocument.createElement('link');524        resourceEl.href = href;525        resourceEl.rel = 'stylesheet';526        resourceEl.dataset['precedence'] = precedence =527          stylesheetDescriptor[j++];528        while ((attr = stylesheetDescriptor[j++])) {529          resourceEl.setAttribute(attr, stylesheetDescriptor[j++]);530        }531        loadingState = resourceEl['_p'] = new Promise((resolve, reject) => {532          resourceEl.onload = cleanupWith.bind(resourceEl, resolve);533          resourceEl.onerror = cleanupWith.bind(resourceEl, reject);534        });535        // Save this resource element so we can bailout if it is used again536        window['$RM'].set(href, resourceEl);537      }538      media = resourceEl.getAttribute('media');539      if (loadingState && (!media || window['matchMedia'](media).matches)) {540        dependencies.push(loadingState);541      }542      if (avoidInsert) {543        // We have a link that is already in the document. We don't want to fall through to the insert path544        continue;545      }546    } else {547      // <style> mode iterates over not-yet-hoisted <style> tags with data-precedence and hoists them.548      resourceEl = styleTagsToHoist[i++];549      if (!resourceEl) {550        // we are done with all style tags551        break;552      }553554      precedence = resourceEl.getAttribute('data-precedence');555      resourceEl.removeAttribute('media');556    }557558    // resourceEl is either a newly constructed <link rel="stylesheet" ...> or a <style> tag requiring hoisting559    const prior = precedences.get(precedence) || lastResource;560    if (prior === lastResource) {561      lastResource = resourceEl;562    }563    precedences.set(precedence, resourceEl);564565    // Finally, we insert the newly constructed instance at an appropriate location566    // in the Document.567    if (prior) {568      prior.parentNode.insertBefore(resourceEl, prior.nextSibling);569    } else {570      const head = thisDocument.head;571      head.insertBefore(resourceEl, head.firstChild);572    }573  }574575  const suspenseIdNodeOuter = document.getElementById(suspenseBoundaryID);576  if (suspenseIdNodeOuter) {577    // Mark this Suspense boundary as queued so we know not to client render it578    // at the end of document load.579    const suspenseNodeOuter = suspenseIdNodeOuter.previousSibling;580    suspenseNodeOuter.data = SUSPENSE_QUEUED_START_DATA;581  }582583  Promise.all(dependencies).then(584    window['$RC'].bind(null, suspenseBoundaryID, contentID),585    window['$RX'].bind(null, suspenseBoundaryID, 'CSS failed to load'),586  );587}588589export function completeSegment(containerID, placeholderID) {590  const segmentContainer = document.getElementById(containerID);591  const placeholderNode = document.getElementById(placeholderID);592  // We always expect both nodes to exist here because, while we might593  // have navigated away from the main tree, we still expect the detached594  // tree to exist.595  segmentContainer.parentNode.removeChild(segmentContainer);596  while (segmentContainer.firstChild) {597    placeholderNode.parentNode.insertBefore(598      segmentContainer.firstChild,599      placeholderNode,600    );601  }602  placeholderNode.parentNode.removeChild(placeholderNode);603}604605// This is the exact URL string we expect that Fizz renders if we provide a function action.606// We use this for hydration warnings. It needs to be in sync with Fizz. Maybe makes sense607// as a shared module for that reason.608const EXPECTED_FORM_ACTION_URL =609  // eslint-disable-next-line no-script-url610  "javascript:throw new Error('React form unexpectedly submitted.')";611612export function listenToFormSubmissionsForReplaying() {613  // A global replay queue ensures actions are replayed in order.614  // This event listener should be above the React one. That way when615  // we preventDefault in React's handling we also prevent this event616  // from queing it. Since React listens to the root and the top most617  // container you can use is the document, the window is fine.618  // eslint-disable-next-line no-restricted-globals619  addEventListener('submit', event => {620    if (event.defaultPrevented) {621      // We let earlier events to prevent the action from submitting.622      return;623    }624    const form = event.target;625    const submitter = event['submitter'];626    let action = form.action;627    let formDataSubmitter = submitter;628    if (submitter) {629      const submitterAction = submitter.getAttribute('formAction');630      if (submitterAction != null) {631        // The submitter overrides the action.632        action = submitterAction;633        // If the submitter overrides the action, and it passes the test below,634        // that means that it was a function action which conceptually has no name.635        // Therefore, we exclude the submitter from the formdata.636        formDataSubmitter = null;637      }638    }639    if (action !== EXPECTED_FORM_ACTION_URL) {640      // The form is a regular form action, we can bail.641      return;642    }643644    // Prevent native navigation.645    // This will also prevent other React's on the same page from listening.646    event.preventDefault();647648    // Take a snapshot of the FormData at the time of the event.649    const formData = new FormData(form, formDataSubmitter);650651    // Queue for replaying later. This field could potentially be shared with multiple652    // Reacts on the same page since each one will preventDefault for the next one.653    // This means that this protocol is shared with any React version that shares the same654    // javascript: URL placeholder value. So we might not be the first to declare it.655    // We attach it to the form's root node, which is the shared environment context656    // where we preserve sequencing and where we'll pick it up from during hydration.657    // If there's no ownerDocument, then this is the document.658    const root = form.ownerDocument || form;659    (root['$$reactFormReplay'] = root['$$reactFormReplay'] || []).push(660      form,661      submitter,662      formData,663    );664  });665}

Code quality findings 42

Remove event listeners when no longer needed to prevent memory leaks
warning performance event-listener-leak
suspenseyImage.addEventListener('load', resolve);
Remove event listeners when no longer needed to prevent memory leaks
warning performance event-listener-leak
suspenseyImage.addEventListener('error', resolve);
Remove event listeners when no longer needed to prevent memory leaks
warning performance event-listener-leak
addEventListener('submit', event => {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (contentNode.parentNode === null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (node && node.nodeType === COMMENT_NODE) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (data === SUSPENSE_END_DATA || data === ACTIVITY_END_DATA) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (depth === 0) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
data === SUSPENSE_START_DATA ||
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
data === SUSPENSE_PENDING_START_DATA ||
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
data === SUSPENSE_QUEUED_START_DATA ||
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
data === SUSPENSE_FALLBACK_START_DATA ||
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
data === ACTIVITY_START_DATA
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (className !== 'auto') {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
CSS.escape(name) !== name ? 'r-' + btoa(name).replace(/=/g, '') : name;
Ensure try blocks have corresponding catch or finally blocks
info correctness try-without-catch
try {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (node.nodeType === COMMENT_NODE) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (data === SUSPENSE_END_DATA) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (depth === 0) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
data === SUSPENSE_START_DATA ||
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
data === SUSPENSE_PENDING_START_DATA ||
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
data === SUSPENSE_QUEUED_START_DATA ||
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
data === SUSPENSE_FALLBACK_START_DATA
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
} else if (node.nodeType === ELEMENT_NODE) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
updateClassName !== 'none' &&
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
ancestorElement.nodeType === ELEMENT_NODE &&
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
ancestorElement.getAttribute('vt-update') !== 'none'
Can lead to race conditions; consider Promises or async/await for better control
info correctness set-timeout
setTimeout(resolve, msUntilTimeout);
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (element.getAttribute('style') === '') {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (document['__reactViewTransition'] === transition) {
Use strict inequality (!==) to prevent type coercion bugs
info correctness loose-inequality
if (errorDigest != null) dataset['dgst'] = errorDigest;
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (window['$RB'].length === 2) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (typeof window['$RT'] !== 'number') {
Be cautious with typeof; it has limitations (e.g., typeof null === 'object')
info correctness typeof-pitfall
if (typeof window['$RT'] !== 'number') {
Can lead to race conditions; consider Promises or async/await for better control
info correctness set-timeout
setTimeout(window['$RV'].bind(null, window['$RB']), msUntilTimeout);
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (node.getAttribute('media') === 'not all') {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (node.tagName === 'LINK') {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (prior === lastResource) {
Chain Promises properly to avoid callback hell
info correctness unresolved-promise
Promise.all(dependencies).then(
Ensure the element exists to avoid null errors
info correctness unchecked-element
const segmentContainer = document.getElementById(containerID);
Ensure the element exists to avoid null errors
info correctness unchecked-element
const placeholderNode = document.getElementById(placeholderID);
Use strict inequality (!==) to prevent type coercion bugs
info correctness loose-inequality
if (submitterAction != null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (action !== EXPECTED_FORM_ACTION_URL) {

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.