Stop the OrdersList refetch loop and the disabledClassName DOM leak - #845
Merged
Merged
Conversation
OrderList fired a fetch roughly once per API round-trip: the effect that calls getCustomerOrders retriggered itself through two independent routes. 1. useCustomerProviderValue memoized the whole context value with `state` among its deps and spread `...state` into it, so every landed fetch produced a new getCustomerOrders identity — and OrderList has that callback in its effect deps. Split into a stable `actions` memo plus a thin outer memo that joins state and actions; the one callback that read `state.addresses` directly now reads it through a ref. The callbacks still change identity when accessToken or customerId change, so a fetch still reruns when the token arrives. 2. defaultSdkSorting was a bare object literal, i.e. a new identity on every render, which refired the effect even with stable callbacks. Memoized on the primitives it derives from. Also stop Address from leaking disabledClassName onto the DOM: it was declared in Props but not destructured, so it survived into the rest spread that lands on a <div>. The children-as-function form still receives it through parentProps. Both regressions arrived in v5 and are absent from 4.29.7. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
✅ Deploy Preview for commercelayer-react-components ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
BillingAddressForm declares fieldEvent in Props but never destructures it, so it survives into the rest spread that lands on the <form>: React does not recognize the `fieldEvent` prop on a DOM element. Its twin already does this correctly — ShippingAddressForm.tsx destructures `fieldEvent: _fieldEvent = "change"` — which is what makes this an omission rather than a decision. Neither component reads the prop: it appears nowhere else in src/, so it is accepted, typed, and inert. Found by scanning for components whose Props extend JSX.IntrinsicElements, declare non-DOM props, and spread the rest onto a DOM element. Only two candidates survived filtering for real HTML attributes, and the other one (AvailabilityTemplate) is a false positive — its extra names live inside a `labels` object that is destructured. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nothing in CI ran the tests. The check that reads "test / test" comes from merge-test-env-check.yml, whose `test` job only lists licenses — it takes three seconds and passes regardless of the code. The 1376 tests across the three packages ran only in the local pre-commit hook, so a PR that broke them showed all green. This workflow is named unit-tests with a vitest job, rendering as "unit-tests / vitest", so it cannot be mistaken for the licenses check sitting next to it. `pnpm test` is not reused: the react-components script starts with `pnpm audit --prod --audit-level high`, which fails on any newly published advisory and would turn the suite red for reasons unrelated to the change. Lint and vitest run directly instead. No build step is needed — the vitest configs alias the sibling workspace packages to their source. Verified both ways locally: the commands exit 0 on this branch, and exit 1 with one of the fixes in this PR reverted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
commit: |
The first run of this workflow failed, which is the workflow doing its job. react-hooks-components' lint script ends in `tsc`, and that resolves @commercelayer/core-components through its dist/ — absent on a clean checkout, so the package produced 30-odd TS2307 errors and the job stopped before reaching vitest. It passed locally only because a previous build had left dist/ in place. netlify.toml warns about exactly this for the storybook build; the comment claiming no build was needed was right about vitest and wrong about lint. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The provider wrapped its value in an object literal, so every render of CommerceLayer gave every consumer a fresh identity. This provider sits at the root of the tree, and consumers put values from it into effect dependency arrays — it was the amplifier behind the OrderList refetch loop rather than its cause, and it stays a trap for anything else downstream. The test needs a fresh child element per render: reusing one element makes React skip rendering the child entirely, and the assertion then measures nothing. It fails with the memo removed. The five other providers built from object literals are left alone: three sit inside .map() callbacks where a per-item memo is awkward and the value is a single loop variable, and the remaining two are narrow leaf contexts. None of them wraps the app. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pfferrari
approved these changes
Sep 16, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was happening
On a page rendering
<OrderList>, the browser fired a request toorders(ororder_subscriptions) roughly once a second, indefinitely. There is no timer involved — the rate simply equals the API round-trip time, because the effect that fetches retriggers itself.Two independent routes closed the cycle, and both had to be cut.
1. The context rebuilt its callbacks on every fetch
useCustomerProviderValuememoized the whole context value withstateamong its dependencies, and spread...stateinto it. So:OrderListcallsgetCustomerOrders→GET /orderssetState({...prev, orders})→ newstateobjectgetCustomerOrdersgets a new identityOrderListhas that callback in its effect deps → the effect runs again → back to 1Fixed by splitting the value in two: a stable
actionsmemo keyed only on what the callbacks actually close over (accessToken,customerId,interceptors,order,pageSize,withoutIncludes), and a thin outer memo that joins state and actions. The single callback that readstate.addressesdirectly (createCustomerAddress) now reads it through a ref.The callbacks still change identity when
accessTokenorcustomerIdchange, so a fetch still reruns when the token arrives. That property is why the callbacks were left inOrderList's dependency array rather than removed from it.2.
defaultSdkSortingwas a fresh object on every renderA bare object literal, so a new identity on every render — enough to refire the effect even if the callbacks were stable. Now memoized on the primitives it derives from.
Where it came from
36a721d9(v5.0.0-beta.0~11) widened the dependency array:4.29.7is not affected.Also in here:
disabledClassNamereaching the DOMAddressdeclareddisabledClassNameinPropsbut did not destructure it, so it survived into the rest spread that lands on a<div>. It came loose in1c85518b, the addresses-domain refactor, which also dropped thedisabledbranch — so in v5 the prop had no effect at all beyond polluting the DOM. It is now pulled out of the spread; the children-as-function form still receives it throughparentProps, and the JSDoc says so.4.29.7is not affected here either.Tests
specs/orders/order-list.refetch-loop.spec.tsxmounts<OrderList>inside the realCustomerprovider, withcolumnsandsortBypassed as inline literals the way a consumer actually passes them.Both new tests were checked against the unfixed source, so they are not vacuous:
getCustomerOrderscallsdisabledClassNameon the DOMFull suite: 1087 tests across 93 files, green.
🤖 Generated with Claude Code