Ssr plugin wip#6800
Conversation
Merging this PR will not alter performance
Comparing Footnotes
|
Greptile SummaryThis PR introduces SSR (Server-Side Rendering) and ISR (Incremental Static Regeneration) support for Reflex apps. A new
Confidence Score: 3/5The PR introduces two new capabilities (SSR state hydration and ISR caching) that are off by default, limiting blast radius — but the on_load handler runner silently drops event payloads, and the The on_load handler invocation calls
Important Files Changed
Reviews (2): Last reviewed commit: "ruff" | Re-trigger Greptile |
| for i, route_part in enumerate(route_parts): | ||
| if i < len(path_parts): | ||
| arg_match = re.match( | ||
| r"^\[{1,2}(?:\.\.\.)?([a-zA-Z_]\w*)\]{1,2}$", route_part | ||
| ) | ||
| if arg_match: | ||
| param_name = arg_match.group(1) | ||
| if param_name in route_args: | ||
| params[param_name] = path_parts[i] |
There was a problem hiding this comment.
Catch-all route captures only one segment
extract_route_params stores only path_parts[i] for every matched dynamic segment, including catch-all routes ([[...splat]]). For a URL like /docs/a/b/c against route docs/[[...splat]], the function assigns {"splat": "a"} instead of capturing all remaining segments (a/b/c). Any on_load handler that reads self.router.page.params["splat"] to fetch content by path will silently receive a truncated value, producing wrong SSR output for every catch-all route.
The fix requires detecting that the matched param has type RouteArgType.LIST (already tracked in route_args) and joining the remaining path parts when that type is seen.
| revalidation endpoint requires a matching ``X-Reflex-ISR-Token`` header when | ||
| the ``REFLEX_ISR_REVALIDATE_TOKEN`` env var is set. | ||
|
|
||
| Run in production as an app factory, e.g. | ||
| ``granian --factory reflex.isr:create_isr_app``. | ||
|
|
||
| Args: |
There was a problem hiding this comment.
Redis tag sets accumulate without expiry
pipe.sadd(self._TAG_PREFIX + tag, key) writes tag index entries with no TTL. When a page's TTL expires naturally in Redis, the corresponding entry in the tag set remains indefinitely. On long-lived deployments with many unique tags or frequent cache churn, these orphaned set members grow without bound. When invalidate_tag is later called, it deletes pages that no longer exist (harmless but wasteful), and the sets themselves are never reaped unless explicitly invalidated.
Consider setting a TTL on each tag key (e.g. EXPIRE tag_key ttl) in the pipeline, or using EXPIREAT set to the furthest expiry of any member.
| result = handler.fn(substate) | ||
| if asyncio.iscoroutine(result): | ||
| result = await result | ||
| if inspect.isgenerator(result): | ||
| for _ in result: | ||
| pass | ||
| elif inspect.isasyncgen(result): | ||
| async for _ in result: |
There was a problem hiding this comment.
Generator handler yields are silently discarded
Generator and async-generator handlers are iterated with for _ in result, which correctly preserves self mutations but silently drops every yielded value. Reflex generator handlers can yield EventSpec objects (e.g. yield SomeState.other_handler()) or explicit state deltas that operate on other substates. Those cross-substate side effects will not be reflected in the SSR output. Apps that use chained generator events for data loading may render with partially hydrated state without any warning in the logs.
#6173