remix-run/remix · error · Error

${name} must return a render function, received ${typeof res

Error message

${name} must return a render function, received ${typeof result}

What it means

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.

Source

Thrown at packages/ui/src/runtime/component.ts:313

  render = (nextProps: ElementProps): [RemixNode, Array<() => void>] => {
    if (this.#removed) {
      console.warn('render called after component was removed, potential application memory leak')
      return [null, []]
    }

    this.#abortRenderSignal()
    syncProps(this.#props, nextProps)

    let renderFn = this.#renderFn

    if (renderFn === undefined) {
      let initialize = this.#config.type as unknown as (handle: Handle<ElementProps, C>) => unknown
      let result = initialize(this.#handle)

      if (!isRenderFn(result)) {
        let name = this.#config.type.name || 'Anonymous'
        throw new Error(`${name} must return a render function, received ${typeof result}`)
      }

      renderFn = result
      this.#renderFn = renderFn
    }

    return [renderFn(), this.#dequeueTasks()]
  }

  remove = (): Array<() => void> => {
    if (this.#removed) return EMPTY_TASKS
    this.#removed = true
    this.#connectedController?.abort()
    this.#abortRenderSignal()
    if (this.#tasks.length === 0) return EMPTY_TASKS
    return this.#dequeueTasks((sharedAbortedSignal ??= AbortSignal.abort()))
  }

View on GitHub (pinned to 9696913134)

Solutions

  1. Wrap the JSX in an arrow function: `return () => (<div>…</div>)`
  2. Make sure every code path in the component body ends with `return () => …` — including early returns in setup logic
  3. Keep setup-only work (state init, subscriptions) above the returned render closure, not instead of it

Example fix

// before
function Greeting(handle: Handle<{ name: string }>) {
  return <div>Hello {handle.props.name}!</div>
}

// after
function Greeting(handle: Handle<{ name: string }>) {
  return () => <div>Hello {handle.props.name}!</div>
}
Defensive patterns

Strategy: type-guard

Type guard

// Remix UI components must return a function; verify in tests:
function returnsRenderFn(component: (handle: any) => unknown): boolean {
  return typeof component({ props: {}, update() {}, queueTask() {}, signal: new AbortSignal(), id: 'x', context: { get() { throw new Error('no ctx') }, set() {} } }) === 'function'
}

Prevention

When it happens

Trigger: 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'.

Common situations: 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.

Related errors


AI-assisted analysis of remix-run/remix@9696913134 (2026-08-27). Data as JSON: /api/errors/9be274eaa46e0170. Report an issue: GitHub.