Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
96 changes: 58 additions & 38 deletions reflex/.templates/web/utils/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,53 @@ export const ReflexEvent = (
return { name, payload, handler, event_actions };
};

/**
* Apply event actions before invoking a target function.
* @param {Function} target The function to invoke after applying event actions.
* @param {Object.<string, (number|boolean)>} event_actions The actions to apply.
* @param {Array<any>|any} args The event args.
* @param {string|null} action_key A stable key for debounce/throttle tracking.
* @param {Function|null} temporal_handler Returns whether temporal actions may run.
* @returns The target result, if it runs immediately.
*/
export const applyEventActions = (
target,
event_actions = {},
args = [],
action_key = null,
temporal_handler = null,
) => {
if (!(args instanceof Array)) {
args = [args];
}

const _e = args.find((o) => o?.preventDefault !== undefined);

if (event_actions?.preventDefault && _e?.preventDefault) {
_e.preventDefault();
}
if (event_actions?.stopPropagation && _e?.stopPropagation) {
_e.stopPropagation();
}
if (event_actions?.temporal && temporal_handler && !temporal_handler()) {
return;
}

const invokeTarget = () => target(...args);
const resolved_action_key = action_key ?? target.toString();

if (event_actions?.throttle) {
if (!throttle(resolved_action_key, event_actions.throttle)) {
return;
}
}
if (event_actions?.debounce) {
debounce(resolved_action_key, invokeTarget, event_actions.debounce);
return;
}
return invokeTarget();
};

/**
* Package client-side storage values as payload to send to the
* backend with the hydrate event
Expand Down Expand Up @@ -898,51 +945,24 @@ export const useEventLoop = (
// Function to add new events to the event queue.
const addEvents = useCallback((events, args, event_actions) => {
const _events = events.filter((e) => e !== undefined && e !== null);
if (!event_actions?.temporal) {
// Reconnect socket if needed for non-temporal events.
ensureSocketConnected();
}

if (!(args instanceof Array)) {
args = [args];
}

event_actions = _events.reduce(
(acc, e) => ({ ...acc, ...e.event_actions }),
event_actions ?? {},
);

const _e = args.filter((o) => o?.preventDefault !== undefined)[0];

if (event_actions?.preventDefault && _e?.preventDefault) {
_e.preventDefault();
}
if (event_actions?.stopPropagation && _e?.stopPropagation) {
_e.stopPropagation();
}
const combined_name = _events.map((e) => e.name).join("+++");
if (event_actions?.temporal) {
if (!socket.current || !socket.current.connected) {
return; // don't queue when the backend is not connected
}
}
if (event_actions?.throttle) {
// If throttle returns false, the events are not added to the queue.
if (!throttle(combined_name, event_actions.throttle)) {
return;
}
}
if (event_actions?.debounce) {
// If debounce is used, queue the events after some delay
debounce(
combined_name,
() =>
queueEvents(_events, socket, false, navigate, () => params.current),
event_actions.debounce,
);
} else {
queueEvents(_events, socket, false, navigate, () => params.current);
if (!event_actions?.temporal) {
// Reconnect socket if needed for non-temporal events.
ensureSocketConnected();
}

return applyEventActions(
() => queueEvents(_events, socket, false, navigate, () => params.current),
event_actions,
args,
_events.map((e) => e.name).join("+++"),
() => !!socket.current?.connected,
);
}, []);

const sentHydrate = useRef(false); // Avoid double-hydrate due to React strict-mode
Expand Down
10 changes: 5 additions & 5 deletions reflex/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -1934,11 +1934,6 @@ async def upload_file(request: Request):
Returns:
StreamingResponse yielding newline-delimited JSON of StateUpdate
emitted by the upload handler.

Raises:
UploadValueError: if there are no args with supported annotation.
UploadTypeError: if a background task is used as the handler.
HTTPException: when the request does not include token / handler headers.
"""
from reflex.utils.exceptions import UploadTypeError, UploadValueError

Expand All @@ -1963,6 +1958,11 @@ async def _create_upload_event() -> Event:

Returns:
The upload event backed by the original temp files.

Raises:
UploadValueError: If there are no uploaded files or supported annotations.
UploadTypeError: If a background task is used as the handler.
HTTPException: If the request is missing token or handler headers.
"""
files = form_data.getlist("files")
if not files:
Expand Down
7 changes: 6 additions & 1 deletion reflex/constants/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ class CompileVars(SimpleNamespace):
IS_HYDRATED = "is_hydrated"
# The name of the function to add events to the queue.
ADD_EVENTS = "addEvents"
# The name of the function to apply event actions before invoking a target.
APPLY_EVENT_ACTIONS = "applyEventActions"
# The name of the var storing any connection error.
CONNECT_ERROR = "connectErrors"
# The name of the function for converting a dict to an event.
Expand Down Expand Up @@ -128,7 +130,10 @@ class Imports(SimpleNamespace):
EVENTS = {
"react": [ImportVar(tag="useContext")],
f"$/{Dirs.CONTEXTS_PATH}": [ImportVar(tag="EventLoopContext")],
f"$/{Dirs.STATE_PATH}": [ImportVar(tag=CompileVars.TO_EVENT)],
f"$/{Dirs.STATE_PATH}": [
ImportVar(tag=CompileVars.TO_EVENT),
ImportVar(tag=CompileVars.APPLY_EVENT_ACTIONS),
],
}


Expand Down
Loading
Loading