Skip to content

Integrate Fiber State Management and Float Ranks#399

Open
GODrums wants to merge 12 commits into
masterfrom
feat/beta-listing-enhancements
Open

Integrate Fiber State Management and Float Ranks#399
GODrums wants to merge 12 commits into
masterfrom
feat/beta-listing-enhancements

Conversation

@GODrums

@GODrums GODrums commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator

Closes #398

Step 2 of #395

Adds state management integration with Fiber and first Fiber-based listing enhancement: float ranks. More enhancements to follow separately.

Screenshot

image

Note

Medium Risk
Relies on undocumented React Fiber/DOM structure and brittle card selectors that may break when Steam updates the beta UI; changes are gated to the beta market only.

Overview
Adds React Steam Market (beta) support by reading listing data from React Fiber instead of legacy g_rgListingInfo / g_rgAssets.

A new getFiberProps helper walks the __reactFiber$ tree to pull typed MarketListing props (listing id, asset, inspect link, wear from asset properties). BetaListingEnhancer continuously injects into beta listing cards when isReactSteamMarket() is true, fetches inspect ItemInfo via the existing float fetcher, and mounts BetaListingRank beside the wear value span (matched by float text). The market listing page script now loads this enhancer alongside the legacy ItemRowWrapper path.

Only a trivial newline fix in mode.ts; legacy market behavior is unchanged.

Reviewed by Cursor Bugbot for commit 48bc033. Configure here.

@GODrums GODrums self-assigned this Jun 5, 2026
@GODrums GODrums changed the title add Fiber management and rank enhancement Integrate Fiber State Management and Float Ranks Jun 5, 2026
@GODrums GODrums marked this pull request as ready for review June 8, 2026 22:01
@GODrums GODrums requested a review from Step7750 June 8, 2026 22:01

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 48bc033. Configure here.

Comment thread src/lib/components/market/react/listing.ts Outdated
Comment thread src/lib/components/market/react/rank.ts
Comment thread src/lib/components/market/react/rank.ts
Base automatically changed from fix/comp-with-steam-beta to master June 9, 2026 22:45

@Step7750 Step7750 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies for the late review!

The fiber parsing is quite nifty, thanks for figuring all that out -- certainly seems required with the lack of context when injecting.

I'm thinking we can potentially move this PR to be more declarative instead of injecting a wrapper that then finds a child element to manually inject into. This would require expanding the InjectionGuard to a degree, but IMO would enable us to directly configure injecting the rank element.

Comment thread src/lib/components/market/react/listing.ts Outdated
Comment on lines +31 to +53
private placeNextToWear(): void {
if (this.injected || !this.itemInfo || !this.card) return;
if (!parseRank(this.itemInfo)) return;

const wearSpan = this.findWearSpan();
if (!wearSpan) return;

this.injected = true;
wearSpan.insertAdjacentElement('afterend', this);
}

private findWearSpan(): HTMLSpanElement | null {
if (!this.targetFloat) return null;

const spans = this.card.querySelectorAll<HTMLSpanElement>('span[style*="pre-wrap"]');
for (const span of spans) {
const text = span.textContent?.trim();
if (!text) continue;
const value = parseFloat(text);
if (!Number.isNaN(value) && Math.abs(value - this.targetFloat) < 1e-6) return span;
}
return null;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally understandable that it is getting quite difficult to figure out which element relates to wear considering we can't rely on DOM siblings being consistent (i.e. some skins have elements like stickers).

However, we're starting to break the pattern of declarative handling (granted, I think some usages of the old market UI also did).

I'm wondering if we can find a middle-ground where InjectionGuard inside of @Inject* can provide some of this context filtering that can't easily be encoded into a query selector. For instance, we'd have a @InjectAppend decorator for BetaListingRank that attempts to target the exact element we want to append to. Since we expect our query selector to collide, we can pass the proposed element into the InjectionGuard and do some context handling to deduce whether a float is there.

As an alternative, to make our lives simpler, we could try to inject into a more stable element. However, you'd lose the flair of having the rank next to the float.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, the imperative handling isn't ideal. I do believe using ReactListingEnhancer as orchestrator is the simplest and most stable approach though.

Without the context from ReactListingEnhancer (i.e. knowing the float we look for), the guard could only search for "a parseable number between 0 and 1", which becomes extremely brittle (e.g. a skin may have an applied name tag with a matching number).
This approach will quickly hit its limits, for example for our paint seed enhancements in #401 ("any number between 0 and 1000") as it wouldn't be able to distinguish between the pattern template and paint seed.

We also have to pass down the fetched listing props and itemInfo from ReactListingEnhancer (here: low_rank, high_rank) as all smaller injections depend on it. Fetching them directly wouldn't be possible.

I would leave the final choice up to you, but the declarative approach would definitely come with much stricter limits along the way, which might not be easily solvable.

Comment thread src/lib/components/market/react/rank.ts Outdated

/** Steam implementation detail: the "key" property on the Fiber node is the listing ID. */
get fiberProps(): MarketListingProps | null {
return getFiberProps<MarketListingProps>(this.card, (fiber) => typeof fiber.key === 'string') ?? null;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I'd expect this to be more stable if we checked that memoizedProps contains listing as a field. Valve could add a ancestor element that is closer with some props that breaks this parsing more easily IMO.

@GODrums GODrums Jun 16, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not quite. The way this traversal works is that we traverse through the original React layers until we reach the base component. From my analysis, their code looks something like this:

function ListingCard({ listing, ... }) {
  return (
    <{/* Some memo/forwardRef wrapper */}>
      <Grid ...>
        <Focusable {/* React.context wrapper for controller / Steam Deck */} ...>
          <div ...> /* ← this is our entry HTML traversal element */
             {/* inner content */}
          </div>
        </Focusable>
      </Grid>
    </{wrapper}>
  );
}

So we basically traverse the non-rendered layers up until we reach the base of the React component. And we know this one will always contain key as it's the unique identifier of the list rendering. Something like this:

function ListingResults({ listings }) {
  return (
    <>
      {listings.map((listing) => (
        <ListingCard
          key={listing.listingid}        // ← this is fiber.key
          listing={listing}
          ...
        />
      ))}
    </>
  );
}

Technically, we could switch to detecting listing within memoizedProps, it shouldn't really matter at all though as there cannot be any React components in our traversal up before key is defined. On the other hand, if Valve decides to rename the listing props later on, we would traverse up until the top root in the worst case, which would make the website pretty laggy for users until we push a fix. Using key we are guaranteed to stop at the list-rendering React component.

@GODrums GODrums requested a review from Step7750 June 16, 2026 21:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Restore listing enhancements in the market beta using a React Fiber compatible state management integration

2 participants