Skip to content

Fix RollbarContext without a payload (#102), make onRender work and document it (#88) - #166

Open
devtools-agent[bot] wants to merge 6 commits into
mainfrom
aicd-bot/rollbar-react-102-88-context
Open

devtools-agent[bot] wants to merge 6 commits into
mainfrom
aicd-bot/rollbar-react-102-88-context

Conversation

@devtools-agent

@devtools-agent devtools-agent Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Fixes #102. Covers the behaviour and docs half of #88; the typings half (context required, onRender added to index.d.ts) is in #162, so #88 can be closed once both have merged.

#162 has merged, and this PR now targets main.

Why

#102: Cannot read properties of undefined (reading 'context'). Neither rollbar.js 2.26.4 nor 3.1.0 sets a default for the payload option. RollbarContext and useRollbarContext both read rollbar.options.payload.context, so either one throws on mount unless the config happens to set payload. While testing the fix I found a second problem with the same cause: on unmount they restore the previous context with configure({ payload: { context: undefined } }). rollbar.js's merge skips undefined values, so the context stayed set after unmount.

#88: RollbarContext doesn't match the docs. By default the context is set in componentDidMount, and React only runs that after the children have rendered and mounted. So an error thrown while the children first render, which is exactly what the README's ErrorBoundary pairing is for, is reported with the previous context. The undocumented onRender prop sets the context during the first render instead. It also had two bugs:

  • it called setState from render, which triggers React's "Cannot update during an existing state transition" warning
  • it ignored any later change to the context prop

Nested contexts (from review). Making onRender follow prop changes meant that changing an outer context overwrote an inner one that was still mounted, because React runs update lifecycles child-first. Nesting was already broken on main without onRender and in the hook: children mount before parents, so the outer context won at mount, and unmounting restored in the wrong order, leaving the inner context set after everything unmounted.

What changed

  • src/context-stack.js (new): the active contexts for each Rollbar client, shared by the component and the hook.
    • Each entry takes an order number on first render. Parents render before children, so the highest number is the innermost context, whatever order React mounts or updates them in.
    • Every change or unmount applies the innermost active context.
    • When the list empties, it restores the context from before the first entry, as previous ?? ''.
    • Reads rollbar.options.payload?.context, so a config without payload works.
  • src/rollbar-context.js
    • Adds and removes its entry in the stack. The entry lives on the instance rather than in state, so onRender no longer calls setState during render.
    • Under onRender, the entry is added during the first render, and componentDidUpdate applies a changed context prop.
    • Without onRender: added on mount, re-applied on update, removed on unmount, as before.
  • src/hooks/use-rollbar-context.js: uses the same stack: adds its entry in an effect, updates it when ctx changes, and removes it on unmount.
  • README.md
    • Corrects the claim that RollbarContext "works for your ErrorBoundary".
    • New "Setting the context before children render" section that documents onRender. It explains why onRender isn't the default: it's a side effect in render, and nothing restores the context if React throws that render away (for example an interrupted transition).
    • A note that useRollbarContext has the same timing as the default.
    • Nested contexts are supported, and the innermost one wins.
  • src/tests/components/rollbar-context.test.tsx: new tests. They're TSX and use Fix historyContext and RollbarContext typings, type-check index.d.ts in CI (#69) #162's RollbarContext typings (onRender), so ts-jest and npm run typecheck type-check them.

Why '' rather than undefined or null on restore

configure() ignores undefined. null would be sent as the string "null", because buildPayload in apiUtility.js stringifies any non-string context. For an unset context, buildPayload already sends '' (contextResult.value || ''), so restoring '' produces the same item as never having set one. I checked the merge and buildPayload behaviour in both rollbar 2.26.4 and 3.1.0.

Not changed: making onRender the default

That would fix the ErrorBoundary case for everyone. But it would move a global side effect into render for every user, with the leak described above under concurrent rendering. I think that deserves its own decision rather than riding along with a bug fix.

Validation

Node 20.19, React 17, rollbar 2.26.4 (the repo's dev dependency):

  • npm run typecheck: 11 files, no errors
  • npx jest: 25/25 passing (4 suites, 12 new tests)
  • Run against main's src/rollbar-context.js and src/hooks/use-rollbar-context.js, 8 of the new tests fail:
  • The ErrorBoundary tests pin down <RollbarContext /> implementation doesn't match docs #88's behaviour: a render error is reported with root by default and with home under onRender.
  • npm run lint -- --max-warnings 0: pass
  • npm run lint:examples and npm run test:examples: pass, with the examples using this branch's build
  • Prettier: all changed files pass
  • npm run build: pass. ?. and ?? are transpiled in dist/ and lib/.

🤖 Generated with Claude Code

@devtools-agent
devtools-agent Bot changed the base branch from main to aicd-bot/rollbar-react-69-fix-types September 25, 2026 05:01
@brianr
brianr added this pull request to stack #163 September 25, 2026 05:45
@brianr brianr self-assigned this Sep 25, 2026
AI Agent and others added 2 commits September 25, 2026 11:21
…ocument it (#88)

#102: rollbar.js has no default `payload` option, so RollbarContext and
useRollbarContext threw "Cannot read properties of undefined (reading
'context')" unless the config set one. Read it with `?.`. Restoring the
unset context on unmount now uses '' instead of undefined, which
configure() ignores, so the context used to stay set after unmount.
rollbar.js sends '' for an unset context anyway.

#88: by default the context is set on mount, after the children have
rendered, so errors an ErrorBoundary catches during the first render
are reported with the previous context. `onRender` sets it first, but
it was undocumented and had bugs:
- it called setState during render, which React warns about
- it ignored later changes to the `context` prop
The previous context is now kept on the instance. componentDidUpdate
applies a changed `context` under onRender. The README documents
onRender, when to use it and why it isn't the default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tests were plain JS only because index.d.ts on main had no
`onRender`. Stacked on #162 they type-check as TSX, like the other
component tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brianr
brianr force-pushed the aicd-bot/rollbar-react-102-88-context branch from 6f02701 to e5456cd Compare September 25, 2026 18:21
Base automatically changed from aicd-bot/rollbar-react-69-fix-types to main September 25, 2026 19:23
@aborek-rollbar

Copy link
Copy Markdown

[P2] Preserve the innermost context when an outer context updates — src/rollbar-context.js:46

With nested RollbarContext components using onRender, changing the outer component’s context prop overwrites the still-mounted inner context. React runs update lifecycles child-first, so the unchanged inner component does nothing, then the outer component calls changeContext(), leaving Rollbar with the outer value. Subsequent errors inside the inner subtree are therefore reported with the wrong context. Please preserve nesting order and add a regression test that rerenders the outer context while the inner context remains mounted.

Review feedback: with nested onRender contexts, changing the outer
`context` prop overwrote the inner one that was still mounted. React
runs update lifecycles child-first, so the outer component applied its
value last. Nesting was already broken without onRender and in the
hook: children mount first, so the outer context won at mount, and
unmounting restored in the wrong order, leaving the inner context set.

The component and the hook now share a list of active contexts per
Rollbar client (src/context-stack.js). Each entry takes an order number
on first render, which ranks parents before children, and the innermost
active entry is applied on every change. When the list empties, the
context from before the first entry is restored.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@brianr

brianr commented Sep 25, 2026

Copy link
Copy Markdown
Member

[P2] Preserve the innermost context when an outer context updates

Thanks, confirmed and fixed in d722ee7.

This PR caused the onRender case: on main, onRender ignored every prop change, which happened to keep the inner context. Checking it turned up more nesting bugs that were already on main, in the default mode and in useRollbarContext:

Outer + inner, starting from root Expected onRender (before d722ee7) Default and hook (also on main)
Mount inner inner outer: children mount first
Outer changes to outer2 inner outer2: this comment outer2
Unmount inner outer2 outer: old value hook: root
Unmount all root root inner: left behind

Fix: the component (with or without onRender) and the hook now share one list of active contexts per Rollbar client, in src/context-stack.js.

  • Each entry takes an order number on first render. Parents render before children, so the highest number is the innermost context, whatever order React mounts or updates them in.
  • Every change or unmount applies the innermost active context. When the list empties, the context from before the first one is restored ('' if there wasn't one, as in the Cannot read properties of undefined (reading 'context') #102 fix).

Regression test: nested contexts runs one sequence in four modes: default, onRender, the hook, and the hook inside the component. It mounts both, changes the outer context while the inner one stays mounted, changes the inner one, unmounts it (the outer one's current value should apply), remounts it, then unmounts everything and expects root. All 4 fail on the previous commit; the onRender one fails at your step, with outer2 instead of inner.

npx jest 25/25, typecheck, lint (--max-warnings 0), Prettier and build all pass. The README now says nested contexts are supported and the innermost one wins.

🤖 Generated with Claude Code

CI's examples lint (eslint-plugin-react-hooks 7) rejects the hook
mutating the entry object it kept in useState ("Cannot modify local
variables after render completes"). The stack now maps each order
number to its context, and callers pass the context in, so nothing
held by React is mutated.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@brianr

brianr commented Sep 25, 2026

Copy link
Copy Markdown
Member

Follow-up: CI failed on d722ee7. The examples lint (eslint-plugin-react-hooks 7) flagged the hook for mutating an object it kept in useState. 7eced8f changes the stack to map each order number to its context, so nothing is mutated; the behaviour and tests are unchanged. CI passes on 7eced8f.

🤖 Generated with Claude Code

@rollbar-circleci-machine rollbar-circleci-machine left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AI Agent Review (openai, openai-astra)

Review of #166: one medium-severity bug. The payload crash fix, the context stack and the hook rewrite are correct. With onRender, however, an error that an ErrorBoundary catches on first render leaves an entry in the context stack that is never removed, and that entry blocks every context set before it. Tests were not run.

What checks out:

  • Missing payload (#102): rollbar.js 3.1.0 merge() skips undefined values (node_modules/rollbar/src/utility.js:865). So the old restore to undefined did leave the context in place, and restoring with stack.base ?? '' fixes it. options.payload?.context handles a config with no payload.
  • Hook: splitting useRollbarContext into a set effect ([ctx]) and a remove-only cleanup ([]) is correct on update and unmount, and works with React 18 StrictMode's unmount/remount. useRollbar() and getRollbarFromContext return the same instance, so the class and the hook share one stack.
  • Class component: keeping order/active on the instance avoids calling setState during render. Resetting active in componentWillUnmount makes StrictMode's simulated remount apply the context again.
  • Ordering: order numbers are handed out during render, parents before children, so "innermost wins" holds even though children mount first.

Finding: with onRender, the stack entry is added inside render() and removed only in componentWillUnmount. When the ErrorBoundary catches an error from a child's first render, React throws the RollbarContext away without committing it. The entry is never removed and outranks every context set before it, for the life of the client (details inline).

Outside the findings: during server rendering componentWillUnmount never runs. If an app passes a shared, module-level Rollbar instance to Provider, each server render of an onRender RollbarContext would add another entry that is never removed. The examples in this repo use the per-Provider config prop, so this depends on how an app is set up.

Comment thread src/rollbar-context.js
Review feedback: with onRender, the stack entry was added in render()
and only removed in componentWillUnmount. When an ErrorBoundary around
the RollbarContext catches an error from a child's first render, React
never mounts the RollbarContext, so the entry stayed in the stack for
good and outranked every other context. The same happened on the
server, where nothing mounts.

onRender now sets the context directly in render(), and the component
joins the stack on mount. A microtask queued from render() applies the
context of whatever is mounted again. React reports the error during
the commit, before the microtask runs, and rollbar.js captures its
options when rollbar.error() is called, so the report keeps this
context. This also fixes the leak on main when no other context is
mounted.

The README now recommends putting RollbarContext outside the
ErrorBoundary: React removes everything inside the boundary before the
error is reported, so a RollbarContext inside it can't apply to errors
after the first render.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@rollbar-circleci-machine

Copy link
Copy Markdown
Contributor

AI Agent Review (openai, openai-astra)

This PR replaces the per-component save/restore of payload.context with one context stack per Rollbar client (src/context-stack.js). It fixes #102: nothing reads options.payload.context without a payload any more, and an unset context is restored as '', because rollbar 3.1.0's merge skips undefined values (node_modules/rollbar/src/utility.js:865). It also makes onRender set the context during render without calling setState, and restores the mounted context in a microtask.

I traced the stack logic against React 17's commit order: nesting order, mount before unmount, key-change remounts, discarded renders, and renderToString. I found no correctness problems. I also walked each new test case by hand and they are consistent with the code. I did not run them; this review had no shell.

One gap remains, reported inline and low severity. With onRender, a context prop change on an already-mounted instance is applied only in componentDidUpdate. A child ErrorBoundary reports before that runs, so an error during that update is reported with the old context.

Other notes, not findings:

  • Without onRender, componentDidUpdate still calls configure() on every re-render. That matches the old behaviour.
  • The charter's ruff and pre-commit lint steps don't apply to this JS repo.

@rollbar-circleci-machine

Copy link
Copy Markdown
Contributor

AI Agent Review (openai, openai-astra)

Review: RollbarContext stack, the no-payload fix (#102) and onRender (#88)

No defects confirmed on the changed lines. I read src/context-stack.js, src/rollbar-context.js, src/hooks/use-rollbar-context.js, the new test file and README, and the code they depend on: src/provider.js, src/error-boundary.js, and rollbar 3.1.0's configure/merge/transforms under node_modules/rollbar/src.

What I checked and found correct

  • Cannot read properties of undefined (reading 'context') #102 (no payload): rollbar.options.payload?.context no longer throws. rollbar.js's merge skips undefined values (node_modules/rollbar/src/utility.js:865), so restoring undefined would silently keep the old context. Restoring stack.base ?? '' is the right workaround. In the browser, '' is sent exactly like an unset context (node_modules/rollbar/src/apiUtility.js:4-9).
  • Ordering: the order number is taken on first render, in a class field or a useState initializer. Parents render before their children, so the numbers rank nested contexts from outermost to innermost even though children mount first. This still holds when React throws a render away and starts again, because the new instance or state gets a fresh, higher number.
  • onRender microtask: the check stacks.get(rollbar) === stack is correct in each case I traced:
    • the component mounts: its context stays;
    • the render is thrown away (for example by an outer ErrorBoundary) or rendered on the server: the previous context comes back;
    • the stack is emptied and a new one created before the microtask runs: the stale microtask does nothing.
  • Commit order matches the README: a child ErrorBoundary's componentDidCatch runs before the parent RollbarContext's componentDidMount. A RollbarContext inside the boundary has already been unmounted when the error is reported.
  • React 18 StrictMode: the double mount/unmount/mount works for both the class and the hook, because removing the last context deletes the stack and the next setContext captures the base again. The hook's split into a set effect and a cleanup-only effect handles ctx changes and unmount correctly.
  • Types line up with index.d.ts:43-47,80 and rollbar's index.d.ts (options, LogResult). I did not run the tests, so I can't say whether they pass.

Observations, not findings (low impact, or outside the changed lines)

  • The comment at src/context-stack.js:36-37 says "rollbar.js sends '' for an unset context anyway". That is true in the browser but not on the server. There, addRequestData derives item.data.context from the Express route (node_modules/rollbar/src/server/transforms.js:139-147), and addPayloadOptions runs afterwards (node_modules/rollbar/src/server/rollbar.js:614,618). It merges payload.context === '' over that route (node_modules/rollbar/src/transforms.js:19).
    • So if a shared server instance with no payload.context is passed to <Provider instance> during SSR with an onRender context, it keeps '' afterwards. That hides route contexts on later errors reported with a request.
    • This is niche: the examples pass config, which makes a fresh instance per Provider. It is also better than before the PR, which left the onRender context set on the server for good. Consider rewording the comment, or deleting the key when the base was unset.
  • historyContext (src/history-context.js:37) and useRollbarConfiguration still call rollbar.configure({ payload: { context } }) directly, bypassing the stack. When the last RollbarContext unmounts, the stack restores the base it captured on first mount, which overwrites anything they set in between. The old code did the same, so this is not a regression.
  • With onRender, a later change to the context prop is applied in componentDidUpdate, after the children have re-rendered. Only the first render gets the early context. The README wording is consistent with this.

Restoring an unset context as '' is only equivalent to leaving it unset in
the browser. On the server, rollbar.js derives the context from the Express
route in addRequestData, and addPayloadOptions then merges payload.context,
including '', over it. So after an onRender server render with a shared
instance whose config has no payload.context, later request errors lose
their route context. rollbar.js has no way to remove the key (configure()
skips undefined and the notifier keeps its own merged copy), so correct the
comment and document it in the README instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cannot read properties of undefined (reading 'context')

3 participants