Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Don't forget to remove deprecated code on each major release!
- The `name` argument has been renamed to `channel`.
- The `group_name` argument has been renamed to `group`.
- The `group_add` and `group_discard` arguments have been removed for simplicity.
- To improve performance, `preact` is now used as the default client-side library instead of `react`.

### [5.2.1] - 2025-01-10

Expand Down
Binary file modified src/js/bun.lockb
Binary file not shown.
10 changes: 6 additions & 4 deletions src/js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,18 @@
"check": "prettier --check . && eslint"
},
"devDependencies": {
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"eslint": "^9.13.0",
"eslint-plugin-react": "^7.37.1",
"prettier": "^3.3.3"
"prettier": "^3.3.3",
"bun-types": "^0.5.0"
},
"dependencies": {
"@pyscript/core": "^0.6",
"@reactpy/client": "^0.3.2",
"event-to-object": "^0.1.2",
"morphdom": "^2.7.4"
"morphdom": "^2.7.4",
"preact": "^10.26.9",
"react": "npm:@preact/compat@17.1.2",
"react-dom": "npm:@preact/compat@17.1.2"
}
}
6 changes: 3 additions & 3 deletions src/js/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {
BaseReactPyClient,
ReactPyClient,
ReactPyModule,
type ReactPyClient,
type ReactPyModule,
} from "@reactpy/client";
import { createReconnectingWebSocket } from "./utils";
import { ReactPyDjangoClientProps, ReactPyUrls } from "./types";
import type { ReactPyDjangoClientProps, ReactPyUrls } from "./types";

export class ReactPyDjangoClient
extends BaseReactPyClient
Expand Down
50 changes: 28 additions & 22 deletions src/js/src/components.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,54 @@
import { DjangoFormProps, HttpRequestProps } from "./types";
import React from "react";
import ReactDOM from "react-dom";
import type { DjangoFormProps, HttpRequestProps } from "./types";
import { useEffect } from "preact/hooks";
import { type ComponentChildren, render, createElement } from "preact";
/**
* Interface used to bind a ReactPy node to React.
*/
export function bind(node) {
export function bind(node: HTMLElement | Element | Node) {
return {
create: (type, props, children) =>
React.createElement(type, props, ...children),
render: (element) => {
ReactDOM.render(element, node);
create: (
type: string,
props: Record<string, unknown>,
children: ComponentChildren[],
) => createElement(type, props, ...children),
render: (element: HTMLElement | Element | Node) => {
render(element, node);
},
unmount: () => ReactDOM.unmountComponentAtNode(node),
unmount: () => render(null, node),
};
}

export function DjangoForm({
onSubmitCallback,
formId,
}: DjangoFormProps): null {
React.useEffect(() => {
useEffect(() => {
const form = document.getElementById(formId) as HTMLFormElement;

// Submission event function
const onSubmitEvent = (event) => {
const onSubmitEvent = (event: Event) => {
event.preventDefault();
const formData = new FormData(form);

// Convert the FormData object to a plain object by iterating through it
// If duplicate keys are present, convert the value into an array of values
const entries = formData.entries();
const formDataArray = Array.from(entries);
const formDataObject = formDataArray.reduce((acc, [key, value]) => {
if (acc[key]) {
if (Array.isArray(acc[key])) {
acc[key].push(value);
const formDataObject = formDataArray.reduce<Record<string, unknown>>(
(acc, [key, value]) => {
if (acc[key]) {
if (Array.isArray(acc[key])) {
acc[key].push(value);
} else {
acc[key] = [acc[key], value];
}
} else {
acc[key] = [acc[key], value];
acc[key] = value;
}
} else {
acc[key] = value;
}
return acc;
}, {});
return acc;
},
{},
);

onSubmitCallback(formDataObject);
};
Expand All @@ -64,7 +70,7 @@ export function DjangoForm({
}

export function HttpRequest({ method, url, body, callback }: HttpRequestProps) {
React.useEffect(() => {
useEffect(() => {
fetch(url, {
method: method,
body: body,
Expand Down
11 changes: 8 additions & 3 deletions src/js/src/mount.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { ReactPyDjangoClient } from "./client";
import React from "react";
import ReactDOM from "react-dom";
import { render } from "preact";
import { Layout } from "@reactpy/client/src/components";

export function mountComponent(
Expand Down Expand Up @@ -76,5 +75,11 @@ export function mountComponent(
}

// Start rendering the component
ReactDOM.render(<Layout client={client} />, client.mountElement);
if (client.mountElement) {
render(<Layout client={client} />, client.mountElement);
} else {
console.error(
"ReactPy mount element is undefined, cannot render the component!",
);
}
}
33 changes: 33 additions & 0 deletions src/js/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"compilerOptions": {
"allowImportingTsExtensions": true,
"allowJs": true,
"allowSyntheticDefaultImports": true,
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"jsx": "react-jsx",
"jsxImportSource": "preact",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "Preserve",
"moduleDetection": "force",
"moduleResolution": "bundler",
"noEmit": true,
"noEmitOnError": true,
"noUnusedLocals": true,
"paths": {
"react": ["./node_modules/preact/compat/"],
"react-dom": ["./node_modules/preact/compat/"],
"react-dom/*": ["./node_modules/preact/compat/*"],
"react/jsx-runtime": ["./node_modules/preact/jsx-runtime"]
},
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"target": "ESNext",
"verbatimModuleSyntax": true
}
}
14 changes: 3 additions & 11 deletions tests/test_app/tests/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# ruff: noqa: N802, RUF012
# ruff: noqa: N802, RUF012, T201
import asyncio
import os
import sys
Expand Down Expand Up @@ -117,16 +117,8 @@ def start_playwright_client(cls):
cls.browser = cls.playwright.chromium.launch(headless=bool(headless))
cls.page = cls.browser.new_page()
cls.page.set_default_timeout(10000)
cls.page.on("console", cls.playwright_logging)

@staticmethod
def playwright_logging(msg):
if msg.type == "error":
_logger.error(msg.text)
elif msg.type == "warning":
_logger.warning(msg.text)
elif msg.type == "info":
_logger.info(msg.text)
cls.page.on("console", lambda msg: print(f"CLIENT {msg.type.upper()}: {msg.text}"))
cls.page.on("pageerror", lambda err: print(f"CLIENT EXCEPTION: {err.name}: {err.message}\n{err.stack}"))

@classmethod
def shutdown_playwright_client(cls):
Expand Down