Skip to content

CONCEPT Cited by 1 source

Declarative lifecycle API

Definition

A declarative lifecycle API is a contribution-surface design where a component author describes a fixed set of named phases (e.g. data-dependencies, processing, rendering) but does not execute them — the framework owns execution, ordering, input/output plumbing, and error handling. The author's code reads as configuration, not control flow. The framework is free to reorder, parallelise, cache, memoise, stream, and short-circuit the lifecycle because it sees all the pieces.

Canonical form (Zalando Rendering Engine)

Zalando's Rendering Engine exposes each Renderer as a fluent interface (see also: chained-builder pattern) where successive .withX(...) calls register functions for each lifecycle step (Source: sources/2021-09-08-zalando-micro-frontends-from-fragments-to-renderers-part-2):

export default tile()
  .withQueries(({ entity: { id } }) => ({
    carousel: { query, variables: { id } },
  }))
  .withProcessDependencies(({ data }) => {
    if (data === null) {
      return { action: "error", message: "No collection data found." };
    }
    return {
      action: "render",
      data,
      tiles: { entities: getCollectionEntities(data) },
    };
  })
  .withRender((props) => {
    // React component for the root output
  });

Three lifecycle slots are canonical:

  • withQueries — declares GraphQL data dependencies. The framework fetches them before subsequent slots run.
  • withProcessDependencies — given fetched data, chooses an action (render / redirect / error) and computes downstream inputs including child entities to recur into.
  • withRender — returns the React component tree to render.

The author writes three callbacks. The framework runs them, in order, with the right inputs, and handles everything in between — data fetching, error handling, child-entity resolution, streaming, hydration, A/B test tracking.

Why the framework benefits

Because the framework sees the whole lifecycle up front, it can:

  • Fetch data up front, not inside render. No "render, then fetch, then re-render" waterfalls.
  • Batch requests across Renderers in the same tree (see DataLoader).
  • Stream HTML as each Renderer's output becomes ready (see concepts/streaming-ssr), without the Renderer author needing to know anything about streaming.
  • Isolate failures — a Renderer whose lifecycle returns action: "error" can be swapped for an error card without collapsing the parent tree.
  • Cache or memoise at any lifecycle boundary without the author's cooperation.
  • Insert platform concerns (monitoring, A/B testing, tracking, Web Vitals) around the lifecycle invisibly.

The inversion is: the author says what the unit needs and what it outputs; the framework decides when and how.

Why the contribution surface benefits

  • Small, learnable API — three named hooks, each with a well-defined input/output contract.
  • Typed end-to-end — in Zalando's case, TypeScript types flow through withQuerieswithProcessDependencieswithRender; props in the render function is inferred.
  • No ceremony — the author doesn't import a framework context, wire up a reducer, or subscribe to a lifecycle bus. They just provide callbacks.
  • Platform features land for free — Web Vitals, logging aggregation, OpenTracing are wired in at the framework level with "zero-integration time for the Renderer developers" (Source: sources/2021-09-08-zalando-micro-frontends-from-fragments-to-renderers-part-2).

Contrast with imperative alternatives

  • React function components with hooks: imperative — author calls useState, useEffect, does fetches inline, owns when data arrives. Powerful but burdens the author with orchestration. The framework cannot know ahead of time what data a component needs.
  • Server components / RSC: partially declarative — the server can fetch data before render, but the author still writes the fetch in imperative JS/TS.
  • Redux-style saga/effects: declarative intent (put, call), but spread across an event-driven bus divorced from the component that needs the data.
  • Zalando tile() lifecycle: fully declarative — the component configuration tells the framework everything it needs, and rendering is just one of several hooks.

Tradeoffs

  • Expressiveness ceiling. Once the lifecycle is fixed, anything not accommodated has to ride in a generic escape hatch (React state via hooks inside withRender, or framework-provided Renderer State for re-running lifecycle).
  • Framework becomes load-bearing. Authors no longer own their data-loading path; the framework's fetch layer is a hard dependency.
  • Debugging is harder. "Why didn't my data arrive?" may live inside the framework, not the author's code.

Seen in

Last updated · 476 distilled / 1,218 read