{"record":{"id":"9be274eaa46e0170","repo":"remix-run/remix","slug":"name-must-return-a-render-function-received","errorCode":null,"errorMessage":"${name} must return a render function, received ${typeof result}","messagePattern":"(.+?) must return a render function, received (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/ui/src/runtime/component.ts","lineNumber":313,"sourceCode":"\n  render = (nextProps: ElementProps): [RemixNode, Array<() => void>] => {\n    if (this.#removed) {\n      console.warn('render called after component was removed, potential application memory leak')\n      return [null, []]\n    }\n\n    this.#abortRenderSignal()\n    syncProps(this.#props, nextProps)\n\n    let renderFn = this.#renderFn\n\n    if (renderFn === undefined) {\n      let initialize = this.#config.type as unknown as (handle: Handle<ElementProps, C>) => unknown\n      let result = initialize(this.#handle)\n\n      if (!isRenderFn(result)) {\n        let name = this.#config.type.name || 'Anonymous'\n        throw new Error(`${name} must return a render function, received ${typeof result}`)\n      }\n\n      renderFn = result\n      this.#renderFn = renderFn\n    }\n\n    return [renderFn(), this.#dequeueTasks()]\n  }\n\n  remove = (): Array<() => void> => {\n    if (this.#removed) return EMPTY_TASKS\n    this.#removed = true\n    this.#connectedController?.abort()\n    this.#abortRenderSignal()\n    if (this.#tasks.length === 0) return EMPTY_TASKS\n    return this.#dequeueTasks((sharedAbortedSignal ??= AbortSignal.abort()))\n  }\n","sourceCodeStart":295,"sourceCodeEnd":331,"githubUrl":"https://github.com/remix-run/remix/blob/9696913134be3a4423513d2775f7b31d6917c049/packages/ui/src/runtime/component.ts#L295-L331","documentation":"Remix UI components follow a two-phase contract: the component function runs once as setup and must RETURN a render function; that returned function is then called for the initial render and every update. On first render, ComponentRuntime calls the component with `handle` and validates the result with `isRenderFn`; anything else (JSX element, undefined, object) throws.","triggerScenarios":"A component that returns JSX directly (`return <div/>` instead of `return () => <div/>`), returns nothing (missing return), or returns a non-function value like an object or promise. The error message reports the received typeof, e.g. 'object' or 'undefined'.","commonSituations":"Newcomers writing React-style components that return JSX from the component body; refactoring a render closure away so the outer function returns undefined; early `return` statements in the setup phase that skip the render-function return.","solutions":["Wrap the JSX in an arrow function: `return () => (<div>…</div>)`","Make sure every code path in the component body ends with `return () => …` — including early returns in setup logic","Keep setup-only work (state init, subscriptions) above the returned render closure, not instead of it"],"exampleFix":"// before\nfunction Greeting(handle: Handle<{ name: string }>) {\n  return <div>Hello {handle.props.name}!</div>\n}\n\n// after\nfunction Greeting(handle: Handle<{ name: string }>) {\n  return () => <div>Hello {handle.props.name}!</div>\n}","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"// Remix UI components must return a function; verify in tests:\nfunction returnsRenderFn(component: (handle: any) => unknown): boolean {\n  return typeof component({ props: {}, update() {}, queueTask() {}, signal: new AbortSignal(), id: 'x', context: { get() { throw new Error('no ctx') }, set() {} } }) === 'function'\n}","tryCatchPattern":null,"preventionTips":["Always write components as `function X(handle) { …; return () => (…) }`","Every early return in setup must still return a render function","Unit-test each component renders via createRoot().flush() to catch contract violations"],"tags":["remix-ui","component-contract","render-function","typescript"],"backgroundTag":"component-must-return-render-function","analyzedSha":"9696913134be3a4423513d2775f7b31d6917c049","analyzedAt":"2026-08-27T19:55:01.024Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}