Skip to content

Fetch-in-render anti-pattern: component-body fetches cause render loops (UI freeze) and stale data #102

Description

@javahippie

Summary

Several components trigger REST fetches during render (component body) instead of in effects. In a signals architecture this is a footgun: every fetch writes a signal (LOADINGSUCCESS), every write re-renders, and the body fires the fetch again. Depending on the guard, the result is an infinite request/render loop (frozen UI) or stale data.

Incident that surfaced this

After the start-form flow became functional (#90), starting a process from the task list froze the whole UI until reload: StartProcessList fetched list_startable unguarded plus the definition/start-form chain in the component body — a self-sustaining loop of requests and re-renders pegging the main thread. The loop had been latent; it only closed once the chain stopped crashing halfway.

Fix pattern applied there (suggested as the general convention):

useEffect(() => {
  void engine_rest.process_definition.list_startable(state)
}, [])

useEffect(() => {
  if (params.tab == null || definition_key == null) return
  void engine_rest.process_definition.start_form(state, definition_key).then(/* … */)
}, [params.tab, definition_key])

Guard taxonomy found in the codebase

  1. Unguarded body fetch → infinite loop. StartProcessList (fixed as part of Start process via start form is broken end-to-end (undefined fetch, crash on non-embedded forms, submit disabled) #90's flow); pages/Tasks.jsx task-list fetching around line 44 deserves a close look (guard logic is implicit).
  2. if (!signal.value) guard → bounded, but stale. Fires only while the signal is empty — so it never refetches when the parameter changes. Example: components/TaskForm.jsx RenderedFallbackForm (if (!state.api.task.rendered_form.value)) keeps showing the previous task's generated form when switching tasks. Same pattern in pages/TaskForm.jsx and pages/Dashboard.jsx.
  3. Compare-signal guard (last_fetched_filter, loaded_for) → works, but ad-hoc. pages/Processes.jsx uses hand-rolled cache-key signals where effect deps would express the same thing declaratively.

Suggested cleanup

  • Establish as convention: data fetching lives in useEffect with the fetch inputs as dependencies — never in the component body. Guards on signal emptiness are not a substitute, since they break parameter-driven refetches.
  • Migrate the pattern-2 and pattern-1 sites; pattern-3 sites can stay but are candidates for simplification.
  • Related cleanup: pages/TaskForm.jsx appears to be a legacy duplicate of components/TaskForm.jsx (still contains the old formKey.substring(13) logic, cf. EmbeddedHtmlTaskForm: hardcoded substring(13) breaks embedded:deployment: forms and non-root context paths #96) — if it is unrouted dead code, deleting it removes several occurrences at once.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions