Skip to content

Commit c0d16c4

Browse files
authored
Switch from react to preact (#285)
1 parent feacabc commit c0d16c4

File tree

8 files changed

+82
-43
lines changed

8 files changed

+82
-43
lines changed

CHANGELOG.md

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

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

src/js/bun.lockb

-1.85 KB
Binary file not shown.

src/js/package.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,18 @@
55
"check": "prettier --check . && eslint"
66
},
77
"devDependencies": {
8-
"@types/react": "^18.2.48",
9-
"@types/react-dom": "^18.2.18",
108
"eslint": "^9.13.0",
119
"eslint-plugin-react": "^7.37.1",
12-
"prettier": "^3.3.3"
10+
"prettier": "^3.3.3",
11+
"bun-types": "^0.5.0"
1312
},
1413
"dependencies": {
1514
"@pyscript/core": "^0.6",
1615
"@reactpy/client": "^0.3.2",
1716
"event-to-object": "^0.1.2",
18-
"morphdom": "^2.7.4"
17+
"morphdom": "^2.7.4",
18+
"preact": "^10.26.9",
19+
"react": "npm:@preact/[email protected]",
20+
"react-dom": "npm:@preact/[email protected]"
1921
}
2022
}

src/js/src/client.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import {
22
BaseReactPyClient,
3-
ReactPyClient,
4-
ReactPyModule,
3+
type ReactPyClient,
4+
type ReactPyModule,
55
} from "@reactpy/client";
66
import { createReconnectingWebSocket } from "./utils";
7-
import { ReactPyDjangoClientProps, ReactPyUrls } from "./types";
7+
import type { ReactPyDjangoClientProps, ReactPyUrls } from "./types";
88

99
export class ReactPyDjangoClient
1010
extends BaseReactPyClient

src/js/src/components.ts

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,54 @@
1-
import { DjangoFormProps, HttpRequestProps } from "./types";
2-
import React from "react";
3-
import ReactDOM from "react-dom";
1+
import type { DjangoFormProps, HttpRequestProps } from "./types";
2+
import { useEffect } from "preact/hooks";
3+
import { type ComponentChildren, render, createElement } from "preact";
44
/**
55
* Interface used to bind a ReactPy node to React.
66
*/
7-
export function bind(node) {
7+
export function bind(node: HTMLElement | Element | Node) {
88
return {
9-
create: (type, props, children) =>
10-
React.createElement(type, props, ...children),
11-
render: (element) => {
12-
ReactDOM.render(element, node);
9+
create: (
10+
type: string,
11+
props: Record<string, unknown>,
12+
children: ComponentChildren[],
13+
) => createElement(type, props, ...children),
14+
render: (element: HTMLElement | Element | Node) => {
15+
render(element, node);
1316
},
14-
unmount: () => ReactDOM.unmountComponentAtNode(node),
17+
unmount: () => render(null, node),
1518
};
1619
}
1720

1821
export function DjangoForm({
1922
onSubmitCallback,
2023
formId,
2124
}: DjangoFormProps): null {
22-
React.useEffect(() => {
25+
useEffect(() => {
2326
const form = document.getElementById(formId) as HTMLFormElement;
2427

2528
// Submission event function
26-
const onSubmitEvent = (event) => {
29+
const onSubmitEvent = (event: Event) => {
2730
event.preventDefault();
2831
const formData = new FormData(form);
2932

3033
// Convert the FormData object to a plain object by iterating through it
3134
// If duplicate keys are present, convert the value into an array of values
3235
const entries = formData.entries();
3336
const formDataArray = Array.from(entries);
34-
const formDataObject = formDataArray.reduce((acc, [key, value]) => {
35-
if (acc[key]) {
36-
if (Array.isArray(acc[key])) {
37-
acc[key].push(value);
37+
const formDataObject = formDataArray.reduce<Record<string, unknown>>(
38+
(acc, [key, value]) => {
39+
if (acc[key]) {
40+
if (Array.isArray(acc[key])) {
41+
acc[key].push(value);
42+
} else {
43+
acc[key] = [acc[key], value];
44+
}
3845
} else {
39-
acc[key] = [acc[key], value];
46+
acc[key] = value;
4047
}
41-
} else {
42-
acc[key] = value;
43-
}
44-
return acc;
45-
}, {});
48+
return acc;
49+
},
50+
{},
51+
);
4652

4753
onSubmitCallback(formDataObject);
4854
};
@@ -64,7 +70,7 @@ export function DjangoForm({
6470
}
6571

6672
export function HttpRequest({ method, url, body, callback }: HttpRequestProps) {
67-
React.useEffect(() => {
73+
useEffect(() => {
6874
fetch(url, {
6975
method: method,
7076
body: body,

src/js/src/mount.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { ReactPyDjangoClient } from "./client";
2-
import React from "react";
3-
import ReactDOM from "react-dom";
2+
import { render } from "preact";
43
import { Layout } from "@reactpy/client/src/components";
54

65
export function mountComponent(
@@ -76,5 +75,11 @@ export function mountComponent(
7675
}
7776

7877
// Start rendering the component
79-
ReactDOM.render(<Layout client={client} />, client.mountElement);
78+
if (client.mountElement) {
79+
render(<Layout client={client} />, client.mountElement);
80+
} else {
81+
console.error(
82+
"ReactPy mount element is undefined, cannot render the component!",
83+
);
84+
}
8085
}

src/js/tsconfig.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"compilerOptions": {
3+
"allowImportingTsExtensions": true,
4+
"allowJs": true,
5+
"allowSyntheticDefaultImports": true,
6+
"declaration": true,
7+
"declarationMap": true,
8+
"esModuleInterop": true,
9+
"forceConsistentCasingInFileNames": true,
10+
"isolatedModules": true,
11+
"jsx": "react-jsx",
12+
"jsxImportSource": "preact",
13+
"lib": ["ESNext", "DOM", "DOM.Iterable"],
14+
"module": "Preserve",
15+
"moduleDetection": "force",
16+
"moduleResolution": "bundler",
17+
"noEmit": true,
18+
"noEmitOnError": true,
19+
"noUnusedLocals": true,
20+
"paths": {
21+
"react": ["./node_modules/preact/compat/"],
22+
"react-dom": ["./node_modules/preact/compat/"],
23+
"react-dom/*": ["./node_modules/preact/compat/*"],
24+
"react/jsx-runtime": ["./node_modules/preact/jsx-runtime"]
25+
},
26+
"resolveJsonModule": true,
27+
"skipLibCheck": true,
28+
"sourceMap": true,
29+
"strict": true,
30+
"target": "ESNext",
31+
"verbatimModuleSyntax": true
32+
}
33+
}

tests/test_app/tests/utils.py

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# ruff: noqa: N802, RUF012
1+
# ruff: noqa: N802, RUF012, T201
22
import asyncio
33
import os
44
import sys
@@ -117,16 +117,8 @@ def start_playwright_client(cls):
117117
cls.browser = cls.playwright.chromium.launch(headless=bool(headless))
118118
cls.page = cls.browser.new_page()
119119
cls.page.set_default_timeout(10000)
120-
cls.page.on("console", cls.playwright_logging)
121-
122-
@staticmethod
123-
def playwright_logging(msg):
124-
if msg.type == "error":
125-
_logger.error(msg.text)
126-
elif msg.type == "warning":
127-
_logger.warning(msg.text)
128-
elif msg.type == "info":
129-
_logger.info(msg.text)
120+
cls.page.on("console", lambda msg: print(f"CLIENT {msg.type.upper()}: {msg.text}"))
121+
cls.page.on("pageerror", lambda err: print(f"CLIENT EXCEPTION: {err.name}: {err.message}\n{err.stack}"))
130122

131123
@classmethod
132124
def shutdown_playwright_client(cls):

0 commit comments

Comments
 (0)