Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Component as Var type #3732

Merged
merged 38 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
cdf6234
[WiP] Support UI components returned from a computed var
masenf Aug 1, 2024
31018de
Get rid of nasty react hooks warning
masenf Aug 1, 2024
92dd6ba
include @babel/standalone in the base to avoid CDN
masenf Aug 2, 2024
9efda81
put window variables behind an object
adhami3310 Aug 12, 2024
31eac81
Merge branch 'main' into feat/dynamic-components
adhami3310 Sep 11, 2024
eae47dc
use jsx
adhami3310 Sep 11, 2024
b74c024
implement the thing
adhami3310 Sep 12, 2024
350ccc3
cleanup dead test code (#3909)
benedikt-bartscher Sep 11, 2024
80e5972
override dict in propsbase to use camelCase (#3910)
adhami3310 Sep 11, 2024
9139a3b
[REF-3562][REF-3563] Replace chakra usage (#3872)
ElijahAhianyo Sep 12, 2024
76307dc
[ENG-3717] [flexgen] Initialize app from refactored code (#3918)
masenf Sep 13, 2024
454a098
Remove Pydantic from some classes (#3907)
adhami3310 Sep 13, 2024
c465741
Merging
adhami3310 Sep 14, 2024
f350988
fixss
adhami3310 Sep 14, 2024
4b003c3
Merging
adhami3310 Sep 14, 2024
2a025dd
fix field_name
adhami3310 Sep 14, 2024
6508e30
always import react
adhami3310 Sep 14, 2024
e2ab4b8
move func to file
adhami3310 Sep 16, 2024
5ac0828
do some weird things
adhami3310 Sep 16, 2024
0d6fd78
Merging
adhami3310 Sep 16, 2024
49dca79
it's really ruff out there
adhami3310 Sep 17, 2024
c7f9c60
add docs
adhami3310 Sep 17, 2024
b813d77
how does this work
adhami3310 Sep 17, 2024
343eee7
dang it darglint
adhami3310 Sep 17, 2024
9ad3213
merging
adhami3310 Sep 17, 2024
afae7be
fix the silly
adhami3310 Sep 17, 2024
e64a5f2
don't remove computed guy
adhami3310 Sep 17, 2024
e0cbd3e
silly goose, don't ignore var types :D
adhami3310 Sep 17, 2024
dc033e3
update code
adhami3310 Sep 17, 2024
d5d2d4e
put f string on one line
adhami3310 Sep 17, 2024
d497e64
make it deprecated instead of outright killing it
adhami3310 Sep 18, 2024
cc1784a
i hate it
adhami3310 Sep 18, 2024
b1380c8
add imports from react
adhami3310 Sep 19, 2024
20e4c36
merging
adhami3310 Sep 19, 2024
7822f2b
assert it has evalReactComponent
adhami3310 Sep 19, 2024
21bceaf
do things ig
adhami3310 Sep 19, 2024
ef377f6
move get field to global context
adhami3310 Sep 19, 2024
4e84d64
ooops
adhami3310 Sep 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this import all the radix components all the time?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. In this PR we are not using radix that is stored in the window. I assume loading it there takes some additional time (sadly, not sure if there's any way of doing this dynamically).


{% 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
Loading