Under the react-test-renderer package, a React component whose effect runs `inputRef.current.focus()` blows up on a null ref, though the same component works in the browser. Why is the ref null there, and how do you get such a component to render under that renderer?
answer
- no DOM node exists to hand back
- host refs default to null
- second argument to create()
- per-element stand-in, branch on type
- the stub removes the assertion
basics
~20 sThere is no DOM node to attach, so react-test-renderer sets host refs to null by default. Pass the createNodeMock option to TestRenderer.create to return a stand-in object per host element — but a mocked focus() proves nothing about real focus behaviour.
solid answer
~40 sA ref on a host element normally points at whatever the host layer created. This renderer creates plain objects rather than DOM nodes, and it deliberately does not hand those internal objects to your refs, so `ref.current` is `null` and any `ref.current.someMethod()` throws. The escape hatch is the second argument to `TestRenderer.create`: `createNodeMock` is a function that receives the React element and returns whatever you want the ref to be, so you can return `{ focus() {} }` for an input. That unblocks the render, but be honest about what you have bought — you have stubbed out the very interaction you were trying to verify. If the point of the component is that focus lands in the right place, that belongs in a test with a real host, not behind a node mock.
code
javascript · 21 linesconst React = require('react');
const TestRenderer = require('react-test-renderer');
function AutoFocusField() {
const inputRef = React.useRef(null);
React.useEffect(() => {
inputRef.current.focus(); // null without a node mock
}, []);
return React.createElement('input', { ref: inputRef });
}
const renderer = TestRenderer.create(
React.createElement(AutoFocusField),
{
createNodeMock: (element) =>
element.type === 'input' ? { focus() {} } : null,
},
);
console.log(renderer.toJSON().type); // 'input'
// The render survives - but nothing here proves focus reached the field.go deeper
Know that a ref on a host element points at whatever the host created, and that with no DOM there is nothing to point at, so it comes back null.
Explain that createNodeMock is the create() option that supplies a per-element stand-in, and that it applies only to host refs, not to refs used as mutable storage.
Show the judgment call: a node mock unblocks a render but deletes the assertion when the ref access is the behaviour under test, so name which cases move to a real host instead.
Own the wider rule — every double added only to stop a crash quietly removes coverage — and set the team's expectation for when a stub is acceptable versus when the environment must change.
## Where a host ref normally points When you write `<input ref={inputRef} />`, React attaches the *host instance* to `inputRef.current` during commit. Under `react-dom` that instance is the real `HTMLInputElement`, which is why `.focus()`, `.select()`, `.getBoundingClientRect()` and every other DOM method work. The ref is a handle onto whatever the host layer built. ## Why it is null with a DOM-less renderer This renderer's host layer builds plain JavaScript objects, not DOM nodes. Those objects are the renderer's own bookkeeping — they carry `type`, `props` and children, and they have no methods a component would want to call. Rather than expose that internal shape as your ref, the renderer sets host refs to `null` by default. So any component that reaches through a ref explodes on the first property access: ```js useEffect(() => { inputRef.current.focus(); // TypeError: Cannot read properties of null }, []); ``` Note that only *host* refs are affected. A ref on your own component, or a value ref you use as instance storage (`useRef(0)` for a timer id, say), works exactly as it does anywhere — those never involve the host at all. ## The provided escape hatch `TestRenderer.create` takes an options object as its second argument, and `createNodeMock` is a function on it. React calls it with the element being mounted, and whatever you return becomes `ref.current` for that element: ```js TestRenderer.create(element, { createNodeMock: (element) => element.type === 'input' ? { focus() {}, select() {} } : null, }); ``` Branch on `element.type` (or on a prop) so different host elements get different stand-ins, and return `null` for anything you do not care about — that is the default behaviour anyway. ## Two defensive patterns you will also see Sometimes the fix belongs in the component rather than the test. Optional chaining — `inputRef.current?.focus()` — is legitimate when the ref can genuinely be absent in production too, for instance when the element renders conditionally. It is *not* a good way to paper over a test-environment problem: making production code tolerate a situation that only your renderer creates is the test dictating the design. The other pattern is a component that keeps host access behind a small seam — a hook or a callback prop — so tests can substitute it. That is reasonable design in its own right, but again, adopt it because the boundary helps the code, not because a renderer cannot supply a node. ## The judgment an interviewer is listening for Knowing the option exists is the small half of the answer. The large half is noticing what a node mock costs. If the effect calls `.focus()` and you hand it a `focus` that does nothing, the test now asserts that *a function was called on an object you invented*. Whether focus actually lands on the field, survives a re-render, or moves correctly when the dialog closes — the reason anyone wrote that effect — is now unreachable by construction. So the sequence is: use `createNodeMock` when the ref is incidental to what the test is checking, and the component simply must not crash while you verify something else. When the ref access *is* the behaviour under test, do not mock it — render in an environment that has real host nodes and real focus, and assert on the outcome the user would notice. A stub that satisfies a call is not evidence about the thing the call was supposed to do. This generalises past this one package. Every time a test double exists purely to keep a render from crashing, ask which assertion it has quietly removed. Node mocks are an unusually clear case, because the object you invent has exactly the surface you gave it and no behaviour whatsoever.
- Does the same problem affect a ref used purely as mutable storage, like useRef for a timer id?No. That ref never points at a host instance — it is just a mutable box React keeps across renders, so it works identically under any renderer. The null default applies only to refs attached to host elements, where React would otherwise have to expose the renderer's internal objects. Distinguishing the two is a good tell that a candidate understands what a ref actually holds.
- When would you refuse to add a node mock and change the test instead?Whenever the ref access is the behaviour under test — autofocus, scroll-into-view, measurement, focus restoration. Stubbing the method there deletes the only thing worth verifying and leaves a test that asserts your own stub was called. I would move that case to an environment with real host nodes and assert the user-visible outcome, and keep node mocks for refs that are incidental to the assertion at hand.
- Is optional chaining on ref.current a good general fix for this?Only when the ref can legitimately be absent in production, such as an element that renders conditionally. Adding it purely so a renderer stops crashing lets the test environment dictate production code, and it can hide a real ordering bug by turning a loud crash into a silent no-op. Fix the environment when the environment is the problem.
saying these in an interview costs you the question
- Thinks the null ref is a bug in the component
- Believes refs are null because effects have not run
- Assumes a mocked focus() proves focus behaviour
- Adds optional chaining to production code to silence a test crash
- Confuses host refs with useRef used as mutable storage