WIP: feat: native React client via client_type="react" - #69
WIP: feat: native React client via client_type="react"#69patrickoleary wants to merge 2 commits into
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?
State key now holds the payload dict directly instead of a pre-serialized JSON string; client keeps a string parse path for backward compatibility. Drop unused coi-serviceworker from the react app.
|
Pushed 89c4008 — review fixes for the native-dict template payload:
|
|
The hardest thing for non-web developers is mixing javascript and Python. The 'r_*' directives are not for react developers, they are for non-web developers defining there ui on the Python side. So I push back that the directives are totally appropriate for this user base. look at the following examples. Which of these would be more appropriate for the non-web developers. |
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