WIP: feat: native React client via client_type="react" - #69
Conversation
Add a React client (react-app/) as a peer of vue2-app/vue3-app. The server side is unchanged: when client_type == "react", the widget tree serializes to a JSON component tree (utils/react.py) pushed through the same trame__template_* state keys, and the client renders it natively with React.createElement. - widgets/core.py: structured attribute capture alongside the Vue string formatting (vue output byte-identical, regression-tested), react_node property, and r_* directive aliases (r_if/r_show/r_for/r_model/r_bind_*/ r_on_*) for React-flavored apps - react-app/: registry + renderer (expression evaluation against the shared state, conditionals, lists, controlled-component models, scoped slots, event modifiers), 14 built-in component ports, vite build into module/react-www - module/react.py + ui/core.py flush branch for the react client type - examples/react/, serializer unit tests, Playwright e2e, react steps in CI, react-www in wheel data
| @@ -0,0 +1,2 @@ | |||
| /*! coi-serviceworker v0.1.7 - Guido Zuidhof and contributors, licensed under MIT */ | |||
There was a problem hiding this comment.
That file can be removed. We don't need it anymore. No need to keep it for react
| return body, arg, [] | ||
|
|
||
|
|
||
| def _apply_directive(dirs, js_key, expr): |
There was a problem hiding this comment.
Let's remove directives since they are not part of the react logic.
| if isinstance(elem, str): | ||
| return {"tag": "__fragment", "children": split_text(elem)} | ||
|
|
||
| server = getattr(elem, "server", None) |
There was a problem hiding this comment.
If we pass the first if, we should be an abstract element which should have a server even if it is initialized to None...
We could have an explicit is_instance in an assert.
I can be wrong but that code like AI generated with lot of guarding while it should not needed.
| ("r_model_lazy", "v-model.lazy"), | ||
| ("r_model_number", "v-model.number"), | ||
| ("r_model_trim", "v-model.trim"), | ||
| ("r_slot", "v-slot"), |
There was a problem hiding this comment.
remove all r_ as it is not part of react logic
| return "\n".join(out_buffer) | ||
|
|
||
| @property | ||
| def react_node(self): |
There was a problem hiding this comment.
should match the AbstractLayout react() property name.
There was a problem hiding this comment.
actually since in react they are different. The current name make sense.
| name | ||
| for name in self._py_attr.keys() | ||
| if name.startswith("v_model_") or name.startswith("v_bind_") | ||
| if name.startswith(("v_model_", "r_model_", "v_bind_", "r_bind_")) |
|
|
||
| # smart key handling | ||
| if name.startswith("v_model_"): | ||
| if name.startswith(("v_model_", "r_model_")): |
| else: | ||
| js_key = f"v-model:{model_name}{'.' if len(modifiers) else ''}{'.'.join(modifiers)}" | ||
| elif name.startswith("v_bind_"): | ||
| elif name.startswith(("v_bind_", "r_bind_")): |
| } | ||
| return self | ||
|
|
||
| def attrs(self, *names): |
There was a problem hiding this comment.
that method should be revisited to better handle vue vs react logic. Having both at the same time is confusing.
| return f"<{self._elem_name} html-error />" | ||
|
|
||
| @property | ||
| def react_node(self): |
There was a problem hiding this comment.
should match the AbstractLayout react() property name.
There was a problem hiding this comment.
actually since in react they are different. The current name make sense.
| def to_react_template(root): | ||
| """Serialize a layout root into the JSON string pushed to the client""" | ||
| node = root.react_node if hasattr(root, "react_node") else to_react_node(root) | ||
| return json.dumps({"version": 1, "root": node}) |
There was a problem hiding this comment.
Should not convert to a string... We should keep the native structure as it will speedup network and handling on the client side.
| function parsePayload(payload: unknown): TrameJsonNode | null { | ||
| if (!payload) return null; | ||
| try { | ||
| const { root } = JSON.parse(payload as string) as TrameTemplatePayload; |
There was a problem hiding this comment.
Should be the native structure rather than a json string
| stateKey?: string | null; | ||
| }) { | ||
| const trame = useTrame(); | ||
| useTrameState(); // template-wide re-render on any dirty state |
There was a problem hiding this comment.
that full re-render is quite bad, I'm wondering if there is no other path that will make it cleaner. Maybe something based on Zustand for state handling?
Summary
Add a React client (react-app/) as a peer of vue2-app/vue3-app. The server
side is unchanged: when client_type == "react", the widget tree serializes
to a JSON component tree (utils/react.py) pushed through the same
trame__template_* state keys, and the client renders it natively with
React.createElement.
formatting (vue output byte-identical, regression-tested), react_node
property, and r_* directive aliases (r_if/r_show/r_for/r_model/r_bind_/
r_on_) for React-flavored apps
state, conditionals, lists, controlled-component models, scoped slots,
event modifiers), 14 built-in component ports, vite build into
module/react-www