Skip to content

Encapsulate ServerItemRenderer to limit its external API - #4126

Open
handeyeco wants to merge 11 commits into
ref-keypad-contextfrom
server-item-renderer-imperative-handle
Open

handeyeco wants to merge 11 commits into
ref-keypad-contextfrom
server-item-renderer-imperative-handle

Conversation

@handeyeco

@handeyeco handeyeco commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Summary:

I had a random idea this morning: what if we just wrap ServerItemRenderer (SIR) in a forwardRef and use useImperativeHandle to explicitly declare its external API? Turns out we were already wrapping SIR in a forwardRef so all I had to do was (1) stop exporting the inner SIR and (2) explicitly declare the API based on what we're actually using externally.

Before the external API was something like (not including the methods starting with _ like _setCurrentFocus):

// methods
focus
blur
focusPath
blurPath
getDOMNodeForPath
getInputPaths
getNumHints
getPromptJSON
getUserInput
getWidgetIds
getSerializedState

// properties
questionRenderer
props

Now it's:

// methods
focus
blur
getPromptJSON
getUserInput
getWidgetIds
getSerializedState

// properties
questionRenderer

And the methods starting with _ are not accessible at all.

@handeyeco handeyeco self-assigned this Aug 26, 2026
@handeyeco handeyeco changed the title Remove props from RendererInterface Encapsulate ServerItemRenderer to limit its external API Aug 26, 2026
@github-actions

github-actions Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

npm Snapshot: Published

Good news!! We've packaged up the latest commit from this PR (ef50623) and published it to npm. You
can install it using the tag PR4126.

Example:

pnpm add @khanacademy/perseus@PR4126

If you are working in Khan Academy's frontend, you can run the below command.

./dev/tools/bump_perseus_version.ts -t PR4126

If you are working in Khan Academy's webapp, you can run the below command.

./dev/tools/bump_perseus_version.js -t PR4126

Want another snapshot? Comment /snapshot again.

@github-actions

github-actions Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Size Change: +145 B (+0.03%)

Total Size: 520 kB

📦 View Changed
Filename Size Change
packages/perseus/dist/es/index.js 203 kB +145 B (+0.07%)
ℹ️ View Unchanged
Filename Size
packages/kas/dist/es/index.js 20.6 kB
packages/keypad-context/dist/es/index.js 1.07 kB
packages/kmath/dist/es/index.js 6.31 kB
packages/math-input/dist/es/index.js 98.6 kB
packages/math-input/dist/es/strings.js 1.63 kB
packages/perseus-core/dist/es/index.item-splitting.js 13.2 kB
packages/perseus-core/dist/es/index.js 28.8 kB
packages/perseus-editor/dist/es/index.js 106 kB
packages/perseus-linter/dist/es/index.js 10.1 kB
packages/perseus-score/dist/es/index.js 9.91 kB
packages/perseus-utils/dist/es/index.js 403 B
packages/perseus/dist/es/strings.js 12.9 kB
packages/pure-markdown/dist/es/index.js 1.39 kB
packages/simple-markdown/dist/es/index.js 6.12 kB

compressed-size-action

// TODO(LEMS-3185): remove serializedState
blur(): void;
focus(): boolean | null | undefined;
props: any;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This isn't an API that Renderers need to implement, it's just part of React.

const {renderer} = renderQuestion(itemWithMockWidget);

// Act
const node = renderer.getDOMNodeForPath(["mock-widget 1"]);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is no longer an external API. It gets exercised when testing the APIs that use it.

});

// Act
const numHints = renderer.getNumHints();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

getNumHints didn't seem to be used by anything.

</RenderStateRoot>,
<LoadingContext.Provider value={{onRendered}}>
<RenderStateRoot>
<ServerItemRenderer

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The functional ServerItemRenderer wrapper was originally made to consumer LoadingContext. This PR stops exporting the unwrapped, class-based ServerItemRenderer. So to test this functionality we need to use the context provider (which is more realistic anyway).

});

// Act
act(() => renderer.focusPath(["mock-widget 1"]));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is no longer an external API. It gets exercised when testing the APIs that use it.

class Renderer
extends React.Component<Props, State>
implements GetPromptJSONInterface
implements RendererInterface, GetPromptJSONInterface

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I just thought this made sense...

React.useImperativeHandle(
ref,
() => {
const instance = (): ServerItemRenderer => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

A lot of this complexity can go away when we switch SIR entirely to a functional component.

@handeyeco
handeyeco marked this pull request as ready for review August 26, 2026 20:02
@handeyeco
handeyeco requested review from a team, jeremywiebe and mark-fitzgerald August 26, 2026 20:02

@jeremywiebe jeremywiebe 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.

Love it!

};

return {
focus: () => instance().focus(),

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.

Do you think the invariant is worthwhile over just doing this? (I guess I'm asking, do you think it's necessary to throw for unmounted SIRs?)

Suggested change
focus: () => instance().focus(),
focus: () => innerRef.current?.focus(),

@handeyeco handeyeco Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I pushed back against the AI on this too. I think the reason it insists on doing this is type narrowing. The return type for getUserInput is UserInputMap, but doing innerRef.current?.getUserInput() changes it to UserInputMap | undefined. Using invariant narrows the type so innerRef.current can't be undefined.

I could do innerRef.current!.getUserInput() but at least with invariant we get a good error message if something goes horribly wrong. Open to push-back.

EDIT: and also this will go away when we switch SIR to a functional component.

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.

No, I don't have such a strong feeling about it. I see now what it's avoiding and that makes sense. I'll be happy when SIR is functional.

@handeyeco

Copy link
Copy Markdown
Contributor Author

/snapshot

@handeyeco
handeyeco changed the base branch from main to ref-keypad-context August 27, 2026 16:40
@handeyeco
handeyeco force-pushed the server-item-renderer-imperative-handle branch from 516c513 to 4c538ce Compare August 27, 2026 16:44
@handeyeco

Copy link
Copy Markdown
Contributor Author

After making a ZND with the snapshot, there were some issues with infinite rerenders. I'm working on resolving these issues (see #4133)

@handeyeco

Copy link
Copy Markdown
Contributor Author

/snapshot

…text' into server-item-renderer-imperative-handle
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.

3 participants