Skip to content

The render() Method

render() is the one method almost every Lit component implements. This lesson explains its contract, the rules it must follow, and common patterns for keeping it clean.

The render() Contract

render() is called by LitElement whenever a reactive property changes (and once initially, after the component connects). It must return a TemplateResult produced by html, or nothing if there's nothing to display — never a plain string, and never undefined by omission of a return.

Lit expects render() to be a pure function of the component's current state: given the same properties, it should always produce an equivalent template. It should not perform side effects like fetching data or mutating properties directly.

class PriceTag extends LitElement {
  static properties = { amount: { type: Number }, currency: { type: String } };

  render() {
    const formatted = new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: this.currency,
    }).format(this.amount);

    return html`<span class="price">${formatted}</span>`;
  }
}

Formatting derived from amount and currency happens inline in render() — no separate state is stored for the formatted string.

render() Method Signature

render(): TemplateResult | typeof nothing {
  // read this.someProperty
  // return a template, never mutate state here
}
  • render() takes no arguments — read whatever it needs from this (properties and state).
  • Return a template from html, or the nothing sentinel to render nothing at all.
  • Avoid calling this.requestUpdate() or setting reactive properties inside render() — that can trigger render loops.
  • Keep expensive computation memoized or moved to willUpdate() if it doesn't need to run on every single render.

render() Do's and Don'ts

Quick rules for keeping render() predictable.

Do Don't
Return a template from html Return a plain string
Read properties/state Mutate properties/state
Compute derived values inline Perform network requests
Return nothing for empty output Return undefined implicitly
Keep it a pure function of state Rely on external mutable globals

Why render() Should Avoid Side Effects

Because render() can be called more than once for the same logical update in some edge cases, and because its exact timing is managed internally by Lit, any side effect placed inside it (starting a timer, fetching data, mutating a property) risks running more often than you expect, or interacting badly with Lit's update batching.

Data fetching belongs in lifecycle methods like connectedCallback() or in response to a specific property changing (checked inside willUpdate() or updated()), never inside render() itself.

  • Fetch data in connectedCallback() and store the result in a @state() property.
  • React to a specific property changing inside updated(changedProperties).
  • Keep render() limited to reading current state and returning a template.

Returning nothing Instead of Empty Markup

When a component has no visible output for a given state, return Lit's nothing sentinel rather than an empty string or html``. nothing tells Lit to render literally no DOM for that position, which is both clearer and slightly more efficient than an empty template.

import { html, nothing } from 'lit';

render() {
  if (!this.errorMessage) return nothing;
  return html`<p class="error">${this.errorMessage}</p>`;
}

Common Mistakes

  • Fetching data or starting timers inside render(), causing side effects to re-run unpredictably.
  • Mutating a reactive property inside render(), which can trigger an unwanted second update.
  • Returning an empty string instead of the nothing sentinel for 'render nothing' cases.
  • Doing expensive synchronous computation on every render instead of caching results tied to the relevant property.

Key Takeaways

  • render() should be a pure function: same state in, equivalent template out.
  • Never perform side effects (fetches, timers, property mutation) inside render().
  • Use the nothing sentinel, not an empty string, to render nothing for a given branch.
  • Move data fetching and reactions to specific property changes into lifecycle methods instead.

Pro Tip

If you catch yourself writing an if statement inside render() that mutates this.something, that's a strong signal the logic belongs in willUpdate() or updated() instead.