diff --git a/CHANGELOG.md b/CHANGELOG.md index f3a0ff04..53609f21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/js/bun.lockb b/src/js/bun.lockb index 0bb863ce..77925c8f 100644 Binary files a/src/js/bun.lockb and b/src/js/bun.lockb differ diff --git a/src/js/package.json b/src/js/package.json index bfcdb72c..0cdda830 100644 --- a/src/js/package.json +++ b/src/js/package.json @@ -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" } } diff --git a/src/js/src/client.ts b/src/js/src/client.ts index 1f506f56..4a5cfceb 100644 --- a/src/js/src/client.ts +++ b/src/js/src/client.ts @@ -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 diff --git a/src/js/src/components.ts b/src/js/src/components.ts index 176a1f30..6c265ac2 100644 --- a/src/js/src/components.ts +++ b/src/js/src/components.ts @@ -1,17 +1,20 @@ -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, + children: ComponentChildren[], + ) => createElement(type, props, ...children), + render: (element: HTMLElement | Element | Node) => { + render(element, node); }, - unmount: () => ReactDOM.unmountComponentAtNode(node), + unmount: () => render(null, node), }; } @@ -19,11 +22,11 @@ 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); @@ -31,18 +34,21 @@ export function DjangoForm({ // 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>( + (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); }; @@ -64,7 +70,7 @@ export function DjangoForm({ } export function HttpRequest({ method, url, body, callback }: HttpRequestProps) { - React.useEffect(() => { + useEffect(() => { fetch(url, { method: method, body: body, diff --git a/src/js/src/mount.tsx b/src/js/src/mount.tsx index a3a02087..c69d63e8 100644 --- a/src/js/src/mount.tsx +++ b/src/js/src/mount.tsx @@ -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( @@ -76,5 +75,11 @@ export function mountComponent( } // Start rendering the component - ReactDOM.render(, client.mountElement); + if (client.mountElement) { + render(, client.mountElement); + } else { + console.error( + "ReactPy mount element is undefined, cannot render the component!", + ); + } } diff --git a/src/js/tsconfig.json b/src/js/tsconfig.json new file mode 100644 index 00000000..dc3f4f16 --- /dev/null +++ b/src/js/tsconfig.json @@ -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 + } +} diff --git a/tests/test_app/tests/utils.py b/tests/test_app/tests/utils.py index dc3cb9dd..de43d958 100644 --- a/tests/test_app/tests/utils.py +++ b/tests/test_app/tests/utils.py @@ -1,4 +1,4 @@ -# ruff: noqa: N802, RUF012 +# ruff: noqa: N802, RUF012, T201 import asyncio import os import sys @@ -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):