Skip to content

Commit

Permalink
Component as Var type (#3732)
Browse files Browse the repository at this point in the history
* [WiP] Support UI components returned from a computed var

* Get rid of nasty react hooks warning

* include @babel/standalone in the base to avoid CDN

* put window variables behind an object

* use jsx

* implement the thing

* cleanup dead test code (#3909)

* override dict in propsbase to use camelCase (#3910)

* override dict in propsbase to use camelCase

* fix underscore in dict

* dang it darglint

* [REF-3562][REF-3563] Replace chakra usage (#3872)

* [ENG-3717] [flexgen] Initialize app from refactored code (#3918)

* Remove Pydantic from some classes (#3907)

* half of the way there

* add dataclass support

* Forbid Computed var shadowing (#3843)

* get it right pyright

* fix unit tests

* rip out more pydantic

* fix weird issues with merge_imports

* add missing docstring

* make special props a list instead of a set

* fix moment pyi

* actually ignore the runtime error

* it's ruff out there

---------

Co-authored-by: benedikt-bartscher <31854409+benedikt-bartscher@users.noreply.github.com>

* Merging

* fixss

* fix field_name

* always import react

* move func to file

* do some weird things

* it's really ruff out there

* add docs

* how does this work

* dang it darglint

* fix the silly

* don't remove computed guy

* silly goose, don't ignore var types :D

* update code

* put f string on one line

* make it deprecated instead of outright killing it

* i hate it

* add imports from react

* assert it has evalReactComponent

* do things ig

* move get field to global context

* ooops

---------

Co-authored-by: Khaleel Al-Adhami <khaleel.aladhami@gmail.com>
Co-authored-by: benedikt-bartscher <31854409+benedikt-bartscher@users.noreply.github.com>
Co-authored-by: Elijah Ahianyo <elijahahianyo@gmail.com>
  • Loading branch information
4 people authored Sep 20, 2024
1 parent ef38ac2 commit bca49d3
Show file tree
Hide file tree
Showing 14 changed files with 1,397 additions and 165 deletions.
14 changes: 14 additions & 0 deletions reflex/.templates/jinja/web/pages/_app.js.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import '/styles/styles.css'
{% block declaration %}
import { EventLoopProvider, StateProvider, defaultColorMode } from "/utils/context.js";
import { ThemeProvider } from 'next-themes'
import * as React from "react";
import * as utils_context from "/utils/context.js";
import * as utils_state from "/utils/state.js";
import * as radix from "@radix-ui/themes";

{% for custom_code in custom_codes %}
{{custom_code}}
Expand All @@ -26,6 +30,16 @@ function AppWrap({children}) {
}

export default function MyApp({ Component, pageProps }) {
React.useEffect(() => {
// Make contexts and state objects available globally for dynamic eval'd components
let windowImports = {
"react": React,
"@radix-ui/themes": radix,
"/utils/context": utils_context,
"/utils/state": utils_state,
};
window["__reflex"] = windowImports;
}, []);
return (
<ThemeProvider defaultTheme={ defaultColorMode } attribute="class">
<AppWrap>
Expand Down
107 changes: 67 additions & 40 deletions reflex/.templates/web/utils/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from "utils/context.js";
import debounce from "/utils/helpers/debounce";
import throttle from "/utils/helpers/throttle";
import * as Babel from "@babel/standalone";

// Endpoint URLs.
const EVENTURL = env.EVENT;
Expand Down Expand Up @@ -117,8 +118,8 @@ export const isStateful = () => {
if (event_queue.length === 0) {
return false;
}
return event_queue.some(event => event.name.startsWith("reflex___state"));
}
return event_queue.some((event) => event.name.startsWith("reflex___state"));
};

/**
* Apply a delta to the state.
Expand All @@ -129,6 +130,22 @@ export const applyDelta = (state, delta) => {
return { ...state, ...delta };
};

/**
* Evaluate a dynamic component.
* @param component The component to evaluate.
* @returns The evaluated component.
*/
export const evalReactComponent = async (component) => {
if (!window.React && window.__reflex) {
window.React = window.__reflex.react;
}
const output = Babel.transform(component, { presets: ["react"] }).code;
const encodedJs = encodeURIComponent(output);
const dataUri = "data:text/javascript;charset=utf-8," + encodedJs;
const module = await eval(`import(dataUri)`);
return module.default;
};

/**
* Only Queue and process events when websocket connection exists.
* @param event The event to queue.
Expand All @@ -141,7 +158,7 @@ export const queueEventIfSocketExists = async (events, socket) => {
return;
}
await queueEvents(events, socket);
}
};

/**
* Handle frontend event or send the event to the backend via Websocket.
Expand Down Expand Up @@ -208,7 +225,10 @@ export const applyEvent = async (event, socket) => {
const a = document.createElement("a");
a.hidden = true;
// Special case when linking to uploaded files
a.href = event.payload.url.replace("${getBackendURL(env.UPLOAD)}", getBackendURL(env.UPLOAD))
a.href = event.payload.url.replace(
"${getBackendURL(env.UPLOAD)}",
getBackendURL(env.UPLOAD)
);
a.download = event.payload.filename;
a.click();
a.remove();
Expand Down Expand Up @@ -249,7 +269,7 @@ export const applyEvent = async (event, socket) => {
} catch (e) {
console.log("_call_script", e);
if (window && window?.onerror) {
window.onerror(e.message, null, null, null, e)
window.onerror(e.message, null, null, null, e);
}
}
return false;
Expand Down Expand Up @@ -290,10 +310,9 @@ export const applyEvent = async (event, socket) => {
export const applyRestEvent = async (event, socket) => {
let eventSent = false;
if (event.handler === "uploadFiles") {

if (event.payload.files === undefined || event.payload.files.length === 0) {
// Submit the event over the websocket to trigger the event handler.
return await applyEvent(Event(event.name), socket)
return await applyEvent(Event(event.name), socket);
}

// Start upload, but do not wait for it, which would block other events.
Expand Down Expand Up @@ -397,7 +416,7 @@ export const connect = async (
console.log("Disconnect backend before bfcache on navigation");
socket.current.disconnect();
}
}
};

// Once the socket is open, hydrate the page.
socket.current.on("connect", () => {
Expand All @@ -416,7 +435,7 @@ export const connect = async (
});

// On each received message, queue the updates and events.
socket.current.on("event", (message) => {
socket.current.on("event", async (message) => {
const update = JSON5.parse(message);
for (const substate in update.delta) {
dispatch[substate](update.delta[substate]);
Expand Down Expand Up @@ -574,7 +593,11 @@ export const hydrateClientStorage = (client_storage) => {
}
}
}
if (client_storage.cookies || client_storage.local_storage || client_storage.session_storage) {
if (
client_storage.cookies ||
client_storage.local_storage ||
client_storage.session_storage
) {
return client_storage_values;
}
return {};
Expand Down Expand Up @@ -614,15 +637,17 @@ const applyClientStorageDelta = (client_storage, delta) => {
) {
const options = client_storage.local_storage[state_key];
localStorage.setItem(options.name || state_key, delta[substate][key]);
} else if(
} else if (
client_storage.session_storage &&
state_key in client_storage.session_storage &&
typeof window !== "undefined"
) {
const session_options = client_storage.session_storage[state_key];
sessionStorage.setItem(session_options.name || state_key, delta[substate][key]);
sessionStorage.setItem(
session_options.name || state_key,
delta[substate][key]
);
}

}
}
};
Expand Down Expand Up @@ -651,7 +676,7 @@ export const useEventLoop = (
if (!(args instanceof Array)) {
args = [args];
}
const _e = args.filter((o) => o?.preventDefault !== undefined)[0]
const _e = args.filter((o) => o?.preventDefault !== undefined)[0];

if (event_actions?.preventDefault && _e?.preventDefault) {
_e.preventDefault();
Expand All @@ -671,7 +696,7 @@ export const useEventLoop = (
debounce(
combined_name,
() => queueEvents(events, socket),
event_actions.debounce,
event_actions.debounce
);
} else {
queueEvents(events, socket);
Expand All @@ -696,30 +721,32 @@ export const useEventLoop = (
}
}, [router.isReady]);

// Handle frontend errors and send them to the backend via websocket.
useEffect(() => {
if (typeof window === 'undefined') {
return;
}

window.onerror = function (msg, url, lineNo, columnNo, error) {
addEvents([Event(`${exception_state_name}.handle_frontend_exception`, {
// Handle frontend errors and send them to the backend via websocket.
useEffect(() => {
if (typeof window === "undefined") {
return;
}

window.onerror = function (msg, url, lineNo, columnNo, error) {
addEvents([
Event(`${exception_state_name}.handle_frontend_exception`, {
stack: error.stack,
})])
return false;
}
}),
]);
return false;
};

//NOTE: Only works in Chrome v49+
//https://github.com/mknichel/javascript-errors?tab=readme-ov-file#promise-rejection-events
window.onunhandledrejection = function (event) {
addEvents([Event(`${exception_state_name}.handle_frontend_exception`, {
stack: event.reason.stack,
})])
return false;
}

},[])
//NOTE: Only works in Chrome v49+
//https://github.com/mknichel/javascript-errors?tab=readme-ov-file#promise-rejection-events
window.onunhandledrejection = function (event) {
addEvents([
Event(`${exception_state_name}.handle_frontend_exception`, {
stack: event.reason.stack,
}),
]);
return false;
};
}, []);

// Main event loop.
useEffect(() => {
Expand Down Expand Up @@ -782,11 +809,11 @@ export const useEventLoop = (
// Route after the initial page hydration.
useEffect(() => {
const change_start = () => {
const main_state_dispatch = dispatch["reflex___state____state"]
const main_state_dispatch = dispatch["reflex___state____state"];
if (main_state_dispatch !== undefined) {
main_state_dispatch({ is_hydrated: false })
main_state_dispatch({ is_hydrated: false });
}
}
};
const change_complete = () => addEvents(onLoadInternalEvent());
router.events.on("routeChangeStart", change_start);
router.events.on("routeChangeComplete", change_complete);
Expand Down
20 changes: 7 additions & 13 deletions reflex/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ def validate_field_name(bases: List[Type["BaseModel"]], field_name: str) -> None
# shadowed state vars when reloading app via utils.prerequisites.get_app(reload=True)
pydantic_main.validate_field_name = validate_field_name # type: ignore

if TYPE_CHECKING:
from reflex.vars import Var


class Base(BaseModel): # pyright: ignore [reportUnboundVariable]
"""The base class subclassed by all Reflex classes.
Expand Down Expand Up @@ -92,7 +95,7 @@ def set(self, **kwargs):
return self

@classmethod
def get_fields(cls) -> dict[str, Any]:
def get_fields(cls) -> dict[str, ModelField]:
"""Get the fields of the object.
Returns:
Expand All @@ -101,7 +104,7 @@ def get_fields(cls) -> dict[str, Any]:
return cls.__fields__

@classmethod
def add_field(cls, var: Any, default_value: Any):
def add_field(cls, var: Var, default_value: Any):
"""Add a pydantic field after class definition.
Used by State.add_var() to correctly handle the new variable.
Expand All @@ -110,7 +113,7 @@ def add_field(cls, var: Any, default_value: Any):
var: The variable to add a pydantic field for.
default_value: The default value of the field
"""
var_name = var._js_expr.split(".")[-1]
var_name = var._var_field_name
new_field = ModelField.infer(
name=var_name,
value=default_value,
Expand All @@ -133,13 +136,4 @@ def get_value(self, key: str) -> Any:
# Seems like this function signature was wrong all along?
# If the user wants a field that we know of, get it and pass it off to _get_value
key = getattr(self, key)
return self._get_value(
key,
to_dict=True,
by_alias=False,
include=None,
exclude=None,
exclude_unset=False,
exclude_defaults=False,
exclude_none=False,
)
return key
22 changes: 6 additions & 16 deletions reflex/components/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from reflex.base import Base
from reflex.compiler.templates import STATEFUL_COMPONENT
from reflex.components.core.breakpoints import Breakpoints
from reflex.components.dynamic import load_dynamic_serializer
from reflex.components.tags import Tag
from reflex.constants import (
Dirs,
Expand Down Expand Up @@ -52,7 +53,6 @@
ParsedImportDict,
parse_imports,
)
from reflex.utils.serializers import serializer
from reflex.vars import VarData
from reflex.vars.base import LiteralVar, Var

Expand Down Expand Up @@ -615,8 +615,8 @@ def get_event_triggers(self) -> Dict[str, Any]:
if types._issubclass(field.type_, EventHandler):
args_spec = None
annotation = field.annotation
if hasattr(annotation, "__metadata__"):
args_spec = annotation.__metadata__[0]
if (metadata := getattr(annotation, "__metadata__", None)) is not None:
args_spec = metadata[0]
default_triggers[field.name] = args_spec or (lambda: [])
return default_triggers

Expand Down Expand Up @@ -1882,19 +1882,6 @@ def _get_dynamic_imports(self) -> str:
return "".join((library_import, mod_import, opts_fragment))


@serializer
def serialize_component(comp: Component):
"""Serialize a component.
Args:
comp: The component to serialize.
Returns:
The serialized component.
"""
return str(comp)


class StatefulComponent(BaseComponent):
"""A component that depends on state and is rendered outside of the page component.
Expand Down Expand Up @@ -2307,3 +2294,6 @@ def create(cls, *children, **props) -> Component:
update={"disposition": MemoizationDisposition.ALWAYS}
)
return comp


load_dynamic_serializer()
Loading

0 comments on commit bca49d3

Please sign in to comment.