Skip to content

Commit e68da79

Browse files
committed
more intuitive channel layer hook
1 parent 892791e commit e68da79

File tree

9 files changed

+136
-68
lines changed

9 files changed

+136
-68
lines changed

CHANGELOG.md

+7-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,13 @@ Don't forget to remove deprecated code on each major release!
1919

2020
## [Unreleased]
2121

22-
- Nothing (yet)!
22+
### Changed
23+
24+
- Updated the interface for `reactpy.hooks.use_channel_layer` to be more intuitive.
25+
- Arguments now must be provided as keyworded arguments.
26+
- The `name` argument has been renamed to `channel`.
27+
- The `group_name` argument has been renamed to `group`.
28+
- The `group_add` and `group_discard` arguments have been removed for simplicity.
2329

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

docs/examples/python/use_channel_layer.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,17 @@
66
@component
77
def my_component():
88
async def receive_message(message):
9-
set_message(message["text"])
9+
set_message_data(message["text"])
1010

1111
async def send_message(event):
1212
if event["key"] == "Enter":
1313
await sender({"text": event["target"]["value"]})
1414

15-
message, set_message = hooks.use_state("")
16-
sender = use_channel_layer("my-channel-name", receiver=receive_message)
15+
message_data, set_message_data = hooks.use_state("")
16+
sender = use_channel_layer(group="my-group-name", receiver=receive_message)
1717

1818
return html.div(
19-
f"Received: {message}",
19+
f"Received: {message_data}",
2020
html.br(),
2121
"Send: ",
2222
html.input({"type": "text", "onKeyDown": send_message}),

docs/examples/python/use_channel_layer_group.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
@component
77
def my_sender_component():
8-
sender = use_channel_layer(group_name="my-group-name")
8+
sender = use_channel_layer(group="my-group-name")
99

1010
async def submit_event(event):
1111
if event["key"] == "Enter":
@@ -21,10 +21,10 @@ async def submit_event(event):
2121
def my_receiver_component_1():
2222
message, set_message = hooks.use_state("")
2323

24-
async def receive_event(message):
24+
async def receive_message(message):
2525
set_message(message["text"])
2626

27-
use_channel_layer(group_name="my-group-name", receiver=receive_event)
27+
use_channel_layer(group="my-group-name", receiver=receive_message)
2828

2929
return html.div(f"Message Receiver 1: {message}")
3030

@@ -33,9 +33,9 @@ async def receive_event(message):
3333
def my_receiver_component_2():
3434
message, set_message = hooks.use_state("")
3535

36-
async def receive_event(message):
36+
async def receive_message(message):
3737
set_message(message["text"])
3838

39-
use_channel_layer(group_name="my-group-name", receiver=receive_event)
39+
use_channel_layer(group="my-group-name", receiver=receive_message)
4040

4141
return html.div(f"Message Receiver 2: {message}")

docs/examples/python/use_channel_layer_signal_receiver.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77
def my_receiver_component():
88
message, set_message = hooks.use_state("")
99

10-
async def receive_event(message):
10+
async def receive_message(message):
1111
set_message(message["text"])
1212

13-
use_channel_layer("my-channel-name", receiver=receive_event)
13+
# This is defined to receive any messages from both "my-channel-name" and "my-group-name".
14+
use_channel_layer(channel="my-channel-name", group="my-group-name", receiver=receive_message)
1415

1516
return html.div(f"Message Receiver: {message}")

docs/examples/python/use_channel_layer_signal_sender.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ class ExampleModel(Model): ...
1212
def my_sender_signal(sender, instance, **kwargs):
1313
layer = get_channel_layer()
1414

15-
# Example of sending a message to a channel
16-
async_to_sync(layer.send)("my-channel-name", {"text": "Hello World!"})
17-
18-
# Example of sending a message to a group channel
15+
# EXAMPLE 1: Sending a message to a group.
16+
# Note that `group_send` requires using the `group` argument in `use_channel_layer`.
1917
async_to_sync(layer.group_send)("my-group-name", {"text": "Hello World!"})
18+
19+
# EXAMPLE 2: Sending a message to a single channel.
20+
# Note that this is typically only used for channels that use point-to-point communication
21+
async_to_sync(layer.send)("my-channel-name", {"text": "Hello World!"})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from reactpy import component, hooks, html
2+
3+
from reactpy_django.hooks import use_channel_layer
4+
5+
6+
@component
7+
def my_sender_component():
8+
sender = use_channel_layer(channel="my-channel-name")
9+
10+
async def submit_event(event):
11+
if event["key"] == "Enter":
12+
await sender({"text": event["target"]["value"]})
13+
14+
return html.div(
15+
"Message Sender: ",
16+
html.input({"type": "text", "onKeyDown": submit_event}),
17+
)
18+
19+
20+
@component
21+
def my_receiver_component():
22+
message, set_message = hooks.use_state("")
23+
24+
async def receive_message(message):
25+
set_message(message["text"])
26+
27+
use_channel_layer(channel="my-channel-name", receiver=receive_message)
28+
29+
return html.div(f"Message Receiver 1: {message}")

docs/src/reference/hooks.md

+54-22
Original file line numberDiff line numberDiff line change
@@ -309,13 +309,13 @@ This hook utilizes the Django's authentication framework in a way that provides
309309

310310
The `#!python channels.auth.*` functions cannot trigger re-renders of your ReactPy components. Additionally, they do not provide persistent authentication when used within ReactPy.
311311

312-
Django's authentication design requires cookies to retain login status. ReactPy is rendered via WebSockets, and browsers do not allow active WebSocket connections to modify cookies.
312+
This is a result of Django's authentication design, which requires cookies to retain login status. ReactPy is rendered via WebSockets, and browsers do not allow active WebSocket connections to modify cookies.
313313

314314
To work around this limitation, when `#!python use_auth().login()` is called within your application, ReactPy performs the following process...
315315

316-
1. The server authenticates the user into the WebSocket session
317-
2. The server generates a temporary login token linked to the WebSocket session
318-
3. The server commands the browser to fetch the login token via HTTP
316+
1. The server authenticates the user into the WebSocket
317+
2. The server generates a temporary login token
318+
3. The server commands the browser to use the login token (via HTTP)
319319
4. The client performs the HTTP request
320320
5. The server returns the HTTP response, which contains all necessary cookies
321321
6. The client stores these cookies in the browser
@@ -352,7 +352,7 @@ Shortcut that returns the WebSocket or HTTP connection's `#!python User`.
352352

353353
Store or retrieve a `#!python dict` containing arbitrary data specific to the connection's `#!python User`.
354354

355-
This hook is useful for storing user-specific data, such as preferences, settings, or any generic key-value pairs.
355+
This hook is useful for storing user-specific data, such as preferences or settings.
356356

357357
User data saved with this hook is stored within the `#!python REACTPY_DATABASE`.
358358

@@ -397,7 +397,7 @@ User data saved with this hook is stored within the `#!python REACTPY_DATABASE`.
397397

398398
### Use Channel Layer
399399

400-
Subscribe to a [Django Channels layer](https://channels.readthedocs.io/en/latest/topics/channel_layers.html) to send/receive messages.
400+
Subscribe to a [Django Channels Layer](https://channels.readthedocs.io/en/latest/topics/channel_layers.html) to communicate messages.
401401

402402
Layers are a multiprocessing-safe communication system that allows you to send/receive messages between different parts of your application.
403403

@@ -415,30 +415,28 @@ This is often used to create chat systems, synchronize data between components,
415415

416416
| Name | Type | Description | Default |
417417
| --- | --- | --- | --- |
418-
| `#!python name` | `#!python str | None` | The name of the channel to subscribe to. If you define a `#!python group_name`, you can keep `#!python name` undefined to auto-generate a unique name. | `#!python None` |
419-
| `#!python group_name` | `#!python str | None` | If configured, any messages sent within this hook will be broadcasted to all channels in this group. | `#!python None` |
420-
| `#!python group_add` | `#!python bool` | If `#!python True`, the channel will automatically be added to the group when the component mounts. | `#!python True` |
421-
| `#!python group_discard` | `#!python bool` | If `#!python True`, the channel will automatically be removed from the group when the component dismounts. | `#!python True` |
422-
| `#!python receiver` | `#!python AsyncMessageReceiver | None` | An async function that receives a `#!python message: dict` from a channel. If more than one receiver waits on the same channel name, a random receiver will get the result. | `#!python None` |
423-
| `#!python layer` | `#!python str` | The channel layer to use. This layer must be defined in `#!python settings.py:CHANNEL_LAYERS`. | `#!python 'default'` |
418+
| `#!python channel` | `#!python str | None` | The name of the channel this hook will send/receive messages on. If `#!python group` is defined and `#!python channel` is `#!python None`, ReactPy will automatically generate a unique channel name. | `#!python None` |
419+
| `#!python group` | `#!python str | None` | If configured, the `#!python channel` is added to a `#!python group` and any messages sent by `#!python AsyncMessageSender` is broadcasted to all channels within the `#!python group`. | `#!python None` |
420+
| `#!python receiver` | `#!python AsyncMessageReceiver | None` | An async function that receives a `#!python message: dict` from a channel. | `#!python None` |
421+
| `#!python layer` | `#!python str` | The Django Channels layer to use. This layer must be defined in `settings.py:CHANNEL_LAYERS`. | `#!python 'default'` |
424422

425423
<font size="4">**Returns**</font>
426424

427425
| Type | Description |
428426
| --- | --- |
429-
| `#!python AsyncMessageSender` | An async callable that can send a `#!python message: dict`. |
427+
| `#!python AsyncMessageSender` | An async callable that can send messages to the channel(s). This callable accepts a single argument, `#!python message: dict`, which is the data sent to the channel or group of channels. |
430428

431429
??? warning "Extra Django configuration required"
432430

433431
In order to use this hook, you will need to configure Django to enable channel layers.
434432

435433
The [Django Channels documentation](https://channels.readthedocs.io/en/latest/topics/channel_layers.html#configuration) has information on what steps you need to take.
436434

437-
In summary, you will need to:
435+
Here is a short summary of the most common installation steps:
438436

439437
1. Install [`redis`](https://redis.io/download/) on your machine.
440438

441-
2. Run the following command to install `channels-redis` in your Python environment.
439+
2. Install `channels-redis` in your Python environment.
442440

443441
```bash linenums="0"
444442
pip install channels-redis
@@ -457,23 +455,57 @@ This is often used to create chat systems, synchronize data between components,
457455
}
458456
```
459457

460-
??? question "How do I broadcast a message to multiple components?"
458+
??? tip "Learn about the quirks of Django Channel Layers"
459+
460+
ReactPy tries to simplify the process of using Django Channels Layers, but it is important to understand how they work.
461+
462+
There are a few quirks of Django Channels Layers to be aware of:
463+
464+
- Any given `#!python channel` should only have one `#!python receiver` registered to it, under normal circumstances.
465+
- This is why ReactPy automatically generates a unique channel name when using `#!python group`.
466+
- When using `#!python group` within this hook, it is suggested to leave `#!python channel` undefined to let ReactPy automatically create a unique channel name (unless you know what you are doing).
467+
- If you have multiple receivers for the same `#!python channel`, only one receiver will get the result.
468+
- This quirk extends to groups as well. For example, If you have two component instances that use the same `#!python channel` within a `#!python group`, the message will only reach one receiver (for that channel).
469+
- Channels exist independently of their `#!python group`.
470+
- Groups are just a loose collection of channel names where a copy of each message can be sent.
471+
- As a result, Django allows you to send messages directly to a `#!python channel` even if it is within a `#!python group`.
472+
- By default, `#!python RedisChannelLayer` will close groups once they have existed for more than 24 hours.
473+
- You need to create your own subclass of `#!python RedisChannelLayer` to change this behavior.
474+
- By default, `#!python RedisChannelLayer` only allows 100 messages backlogged within a `#!python channel` receive queue.
475+
- Rapidly sending messages can overwhelm this queue, resulting in new messages being dropped.
476+
- If you expect to exceed this limit, you need to create your own subclass of `#!python RedisChannelLayer` to change this behavior.
461477

462-
If more than one receiver waits on the same channel, a random one will get the result.
478+
??? question "How do I broadcast a message to multiple components?"
463479

464-
To get around this, you can define a `#!python group_name` to broadcast messages to all channels within a specific group. If you do not define a channel `#!python name` while using groups, ReactPy will automatically generate a unique channel name for you.
480+
Groups allow you to broadcast messages to all channels within that group. If you do not define a `#!python channel` while using groups, ReactPy will automatically generate a unique channel name for you.
465481

466-
In the example below, all messages sent by the `#!python sender` component will be received by all `#!python receiver` components that exist (across every active client browser).
482+
In the example below, since all components use the same channel `#!python group`, messages sent by `#!python my_sender_component` will reach all existing instances of `#!python my_receiver_component_1` and `#!python my_receiver_component_2`.
467483

468484
=== "components.py"
469485

470486
```python
471487
{% include "../../examples/python/use_channel_layer_group.py" %}
472488
```
473489

490+
??? question "How do I send a message to a single component (point-to-point communication)?"
491+
492+
The most common way of using `#!python use_channel_layer` is to broadcast messages to multiple components via a `#!python group`.
493+
494+
However, you can also use this hook to establish unidirectional, point-to-point communication towards a single `#!python receiver` function. This is slightly more efficient since it avoids the overhead of `#!python group` broadcasting.
495+
496+
In the example below, `#!python my_sender_component` will communicate directly to a single instance of `#!python my_receiver_component`. This is achieved by defining a `#!python channel` while omitting the `#!python group` parameter.
497+
498+
=== "components.py"
499+
500+
```python
501+
{% include "../../examples/python/use_channel_layer_single.py" %}
502+
```
503+
504+
Note that if you have multiple instances of `#!python my_receiver_component` using the same `#!python channel`, only one will receive the message.
505+
474506
??? question "How do I signal a re-render from something that isn't a component?"
475507

476-
There are occasions where you may want to signal a re-render from something that isn't a component, such as a Django model signal.
508+
There are occasions where you may want to signal to the `#!python use_channel_layer` hook from something that isn't a component, such as a Django [model signal](https://docs.djangoproject.com/en/stable/topics/signals/).
477509

478510
In these cases, you can use the `#!python use_channel_layer` hook to receive a signal within your component, and then use the `#!python get_channel_layer().send(...)` to send the signal.
479511

@@ -499,7 +531,7 @@ This is often used to create chat systems, synchronize data between components,
499531

500532
### Use Connection
501533

502-
Returns the active connection, which is either a Django [WebSocket](https://channels.readthedocs.io/en/stable/topics/consumers.html#asyncjsonwebsocketconsumer) or a [HTTP Request](https://docs.djangoproject.com/en/4.2/ref/request-response/#django.http.HttpRequest).
534+
Returns the active connection, which is either a Django [WebSocket](https://channels.readthedocs.io/en/stable/topics/consumers.html#asyncjsonwebsocketconsumer) or a [HTTP Request](https://docs.djangoproject.com/en/stable/ref/request-response/#django.http.HttpRequest).
503535

504536
=== "components.py"
505537

@@ -601,7 +633,7 @@ Shortcut that returns the root component's `#!python id` from the WebSocket or H
601633

602634
The root ID is a randomly generated `#!python uuid4`. It is notable to mention that it is persistent across the current connection. The `uuid` is reset only when the page is refreshed.
603635

604-
This is useful when used in combination with [`#!python use_channel_layer`](#use-channel-layer) to send messages to a specific component instance, and/or retain a backlog of messages in case that component is disconnected via `#!python use_channel_layer( ... , group_discard=False)`.
636+
This is useful when used in combination with [`#!python use_channel_layer`](#use-channel-layer) to send messages to a specific component instance.
605637

606638
=== "components.py"
607639

0 commit comments

Comments
 (0)