Skip to content

Commit

Permalink
dynamic state wip
Browse files Browse the repository at this point in the history
  • Loading branch information
benedikt-bartscher committed Jul 18, 2024
1 parent 25aa0cb commit 8099591
Show file tree
Hide file tree
Showing 10 changed files with 406 additions and 123 deletions.
31 changes: 21 additions & 10 deletions reflex/.templates/jinja/web/utils/context.js.jinja2
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
{% set all_state_names = [] %}
import { createContext, useContext, useMemo, useReducer, useState } from "react"
import { applyDelta, Event, hydrateClientStorage, useEventLoop, refs } from "/utils/state.js"
import { applyDelta, Event, hydrateClientStorage, useEventLoop, refs, createDefaultDict } from "/utils/state.js"

{% if initial_state %}
{% set all_state_names = initial_state.keys() | list %}
export const initialState = {{ initial_state|json_dumps }}
{% else %}
export const initialState = {}
{% endif %}
{% if initial_state_parametrized %}
{% set all_state_names = all_state_names + initial_state_parametrized.keys() | list %}
export const initialStateParametrized = {% raw %}{{% endraw %}{% for state_name, state in initial_state_parametrized.items() %}"{{state_name}}": createDefaultDict(() => ({{state|json_dumps}})){% if not loop.last %},{% endif %}{% endfor %}}
{% else %}
export const initialStateParametrized = {}
{% endif %}

export const defaultColorMode = "{{ default_color_mode }}"
export const ColorModeContext = createContext(null);
export const UploadFilesContext = createContext(null);
export const DispatchContext = createContext(null);
export const StateContexts = {
{% for state_name in initial_state %}
{{state_name|var_name}}: createContext(null),
{% for state_name in all_state_names %}
{{state_name|format_state_name}}: createContext(null),
{% endfor %}
}
export const EventLoopContext = createContext(null);
Expand Down Expand Up @@ -98,25 +106,28 @@ export function EventLoopProvider({ children }) {

export function StateProvider({ children }) {
{% for state_name in initial_state %}
const [{{state_name|var_name}}, dispatch_{{state_name|var_name}}] = useReducer(applyDelta, initialState["{{state_name}}"])
const [{{state_name|format_state_name}}, dispatch_{{state_name|format_state_name}}] = useReducer(applyDelta, initialState["{{state_name}}"])
{% endfor %}
{% for state_name in initial_state_parametrized %}
const [{{state_name|format_state_name}}, dispatch_{{state_name|format_state_name}}] = useReducer(applyDelta, initialStateParametrized["{{state_name}}"])
{% endfor %}
const dispatchers = useMemo(() => {
return {
{% for state_name in initial_state %}
"{{state_name}}": dispatch_{{state_name|var_name}},
{% for state_name in all_state_names %}
"{{state_name}}": dispatch_{{state_name|format_state_name}},
{% endfor %}
}
}, [])

return (
{% for state_name in initial_state %}
<StateContexts.{{state_name|var_name}}.Provider value={ {{state_name|var_name}} }>
{% for state_name in all_state_names %}
<StateContexts.{{state_name|format_state_name}}.Provider value={ {{state_name|format_state_name}} }>
{% endfor %}
<DispatchContext.Provider value={dispatchers}>
{children}
</DispatchContext.Provider>
{% for state_name in initial_state|reverse %}
</StateContexts.{{state_name|var_name}}.Provider>
{% for state_name in all_state_names | reverse %}
</StateContexts.{{state_name|format_state_name}}.Provider>
{% endfor %}
)
}
126 changes: 75 additions & 51 deletions reflex/.templates/web/utils/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,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 @@ -141,7 +141,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 +208,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 +252,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 All @@ -272,7 +275,7 @@ export const applyEvent = async (event, socket) => {
if (socket) {
socket.emit(
"event",
JSON.stringify(event, (k, v) => (v === undefined ? null : v))
JSON.stringify(event, (k, v) => (v === undefined ? null : v)),
);
return true;
}
Expand All @@ -290,10 +293,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 All @@ -302,7 +304,7 @@ export const applyRestEvent = async (event, socket) => {
event.payload.files,
event.payload.upload_id,
event.payload.on_upload_progress,
socket
socket,
);
return false;
}
Expand Down Expand Up @@ -369,7 +371,7 @@ export const connect = async (
dispatch,
transports,
setConnectErrors,
client_storage = {}
client_storage = {},
) => {
// Get backend URL object from the endpoint.
const endpoint = getBackendURL(EVENTURL);
Expand Down Expand Up @@ -397,7 +399,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 Down Expand Up @@ -447,7 +449,7 @@ export const uploadFiles = async (
files,
upload_id,
on_upload_progress,
socket
socket,
) => {
// return if there's no file to upload
if (files === undefined || files.length === 0) {
Expand Down Expand Up @@ -530,8 +532,8 @@ export const uploadFiles = async (
* @param handler The client handler to process event.
* @returns The event object.
*/
export const Event = (name, payload = {}, handler = null) => {
return { name, payload, handler };
export const Event = (name, payload = {}, handler = null, state_key = null) => {
return { name, payload, handler, state_key };
};

/**
Expand All @@ -556,7 +558,7 @@ export const hydrateClientStorage = (client_storage) => {
for (const state_key in client_storage.local_storage) {
const options = client_storage.local_storage[state_key];
const local_storage_value = localStorage.getItem(
options.name || state_key
options.name || state_key,
);
if (local_storage_value !== null) {
client_storage_values[state_key] = local_storage_value;
Expand All @@ -567,14 +569,18 @@ export const hydrateClientStorage = (client_storage) => {
for (const state_key in client_storage.session_storage) {
const session_options = client_storage.session_storage[state_key];
const session_storage_value = sessionStorage.getItem(
session_options.name || state_key
session_options.name || state_key,
);
if (session_storage_value != null) {
client_storage_values[state_key] = session_storage_value;
}
}
}
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 All @@ -588,7 +594,7 @@ export const hydrateClientStorage = (client_storage) => {
const applyClientStorageDelta = (client_storage, delta) => {
// find the main state and check for is_hydrated
const unqualified_states = Object.keys(delta).filter(
(key) => key.split(".").length === 1
(key) => key.split(".").length === 1,
);
if (unqualified_states.length === 1) {
const main_state = delta[unqualified_states[0]];
Expand All @@ -614,15 +620,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 All @@ -640,7 +648,7 @@ const applyClientStorageDelta = (client_storage, delta) => {
export const useEventLoop = (
dispatch,
initial_events = () => [],
client_storage = {}
client_storage = {},
) => {
const socket = useRef(null);
const router = useRouter();
Expand Down Expand Up @@ -685,36 +693,38 @@ export const useEventLoop = (
query,
asPath,
}))(router),
}))
})),
);
sentHydrate.current = true;
}
}, [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 All @@ -731,7 +741,7 @@ export const useEventLoop = (
dispatch,
["websocket", "polling"],
setConnectErrors,
client_storage
client_storage,
);
}
(async () => {
Expand Down Expand Up @@ -764,7 +774,7 @@ export const useEventLoop = (
vars[storage_to_state_map[e.key]] = e.newValue;
const event = Event(
`${state_name}.reflex___state____update_vars_internal_state.update_vars_internal`,
{ vars: vars }
{ vars: vars },
);
addEvents([event], e);
}
Expand All @@ -777,11 +787,11 @@ export const useEventLoop = (
// Route after the initial page hydration.
useEffect(() => {
const change_start = () => {
const main_state_dispatch = dispatch["state"]
const main_state_dispatch = dispatch["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 Expand Up @@ -846,7 +856,7 @@ export const getRefValues = (refs) => {
return refs.map((ref) =>
ref.current
? ref.current.value || ref.current.getAttribute("aria-valuenow")
: null
: null,
);
};

Expand All @@ -865,3 +875,17 @@ export const spreadArraysOrObjects = (first, second) => {
throw new Error("Both parameters must be either arrays or objects.");
}
};

export function createDefaultDict(defaultValueFactory) {
return new Proxy(
{},
{
get: (target, name) => {
if (!(name in target)) {
target[name] = defaultValueFactory();
}
return target[name];
},
},
);
}
Loading

0 comments on commit 8099591

Please sign in to comment.