Skip to content

Commit 96a9f93

Browse files
authored
Cross-process communication via use_channel_layer hook (#221)
This PR develops a `use_channel_layer` hook which provides simplified Django channel layers support.
1 parent 9cacfc3 commit 96a9f93

21 files changed

+434
-4
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ Using the following categories, list your changes in this order:
3434

3535
## [Unreleased]
3636

37-
- Nothing (yet)!
37+
### Added
38+
39+
- Built-in cross-process communication mechanism via the `reactpy_django.hooks.use_channel_layer` hook.
3840

3941
## [3.7.0] - 2024-01-30
4042

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from reactpy import component, hooks, html
2+
from reactpy_django.hooks import use_channel_layer
3+
4+
5+
@component
6+
def my_sender_component():
7+
sender = use_channel_layer("my-channel-name", group=True)
8+
9+
async def submit_event(event):
10+
if event["key"] == "Enter":
11+
await sender({"text": event["target"]["value"]})
12+
13+
return html.div(
14+
"Message Sender: ",
15+
html.input({"type": "text", "onKeyDown": submit_event}),
16+
)
17+
18+
19+
@component
20+
def my_receiver_component_1():
21+
message, set_message = hooks.use_state("")
22+
23+
async def receive_event(message):
24+
set_message(message["text"])
25+
26+
use_channel_layer("my-channel-name", receiver=receive_event, group=True)
27+
28+
return html.div(f"Message Receiver 1: {message}")
29+
30+
31+
@component
32+
def my_receiver_component_2():
33+
message, set_message = hooks.use_state("")
34+
35+
async def receive_event(message):
36+
set_message(message["text"])
37+
38+
use_channel_layer("my-channel-name", receiver=receive_event, group=True)
39+
40+
return html.div(f"Message Receiver 2: {message}")
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from reactpy import component, hooks, html
2+
from reactpy_django.hooks import use_channel_layer
3+
4+
5+
@component
6+
def my_receiver_component():
7+
message, set_message = hooks.use_state("")
8+
9+
async def receive_event(message):
10+
set_message(message["text"])
11+
12+
use_channel_layer("my-channel-name", receiver=receive_event)
13+
14+
return html.div(f"Message Receiver: {message}")
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from asgiref.sync import async_to_sync
2+
from channels.layers import get_channel_layer
3+
from django.db.models import Model
4+
from django.db.models.signals import pre_save
5+
from django.dispatch import receiver
6+
7+
8+
class ExampleModel(Model):
9+
...
10+
11+
12+
@receiver(pre_save, sender=ExampleModel)
13+
def my_sender_signal(sender, instance, **kwargs):
14+
layer = get_channel_layer()
15+
16+
# Example of sending a message to a channel
17+
async_to_sync(layer.send)("my-channel-name", {"text": "Hello World!"})
18+
19+
# Example of sending a message to a group channel
20+
async_to_sync(layer.group_send)("my-channel-name", {"text": "Hello World!"})
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from reactpy import component, hooks, html
2+
from reactpy_django.hooks import use_channel_layer
3+
4+
5+
@component
6+
def my_sender_component():
7+
sender = use_channel_layer("my-channel-name")
8+
9+
async def submit_event(event):
10+
if event["key"] == "Enter":
11+
await sender({"text": event["target"]["value"]})
12+
13+
return html.div(
14+
"Message Sender: ",
15+
html.input({"type": "text", "onKeyDown": submit_event}),
16+
)
17+
18+
19+
@component
20+
def my_receiver_component():
21+
message, set_message = hooks.use_state("")
22+
23+
async def receive_event(message):
24+
set_message(message["text"])
25+
26+
use_channel_layer("my-channel-name", receiver=receive_event)
27+
28+
return html.div(f"Message Receiver: {message}")

docs/src/about/changelog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ hide:
33
- toc
44
---
55

6+
<!--
7+
8+
If you see this page, you probably meant to visit the other CHANGELOG.md (all caps).
9+
10+
-->
11+
612
<p class="intro" markdown>
713

814
{% include-markdown "../../../CHANGELOG.md" start="<!--attr-start-->" end="<!--attr-end-->" %}

docs/src/dictionary.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,4 @@ misconfiguration
3838
misconfigurations
3939
backhaul
4040
sublicense
41+
broadcasted

docs/src/reference/hooks.md

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,11 @@ Mutation functions can be sync or async.
275275

276276
### Use User Data
277277

278-
Store or retrieve data (`#!python dict`) specific to the connection's `#!python User`. This data is stored in the `#!python REACTPY_DATABASE`.
278+
Store or retrieve a `#!python dict` containing user data specific to the connection's `#!python User`.
279+
280+
This hook is useful for storing user-specific data, such as preferences, settings, or any generic key-value pairs.
281+
282+
User data saved with this hook is stored within the `#!python REACTPY_DATABASE`.
279283

280284
=== "components.py"
281285

@@ -312,6 +316,103 @@ Store or retrieve data (`#!python dict`) specific to the connection's `#!python
312316

313317
---
314318

319+
## Communication Hooks
320+
321+
---
322+
323+
### Use Channel Layer
324+
325+
Subscribe to a [Django Channels layer](https://channels.readthedocs.io/en/latest/topics/channel_layers.html) to send/receive messages.
326+
327+
Layers are a multiprocessing-safe communication system that allows you to send/receive messages between different parts of your application.
328+
329+
This is often used to create chat systems, synchronize data between components, or signal re-renders from outside your components.
330+
331+
=== "components.py"
332+
333+
```python
334+
{% include "../../examples/python/use-channel-layer.py" %}
335+
```
336+
337+
??? example "See Interface"
338+
339+
<font size="4">**Parameters**</font>
340+
341+
| Name | Type | Description | Default |
342+
| --- | --- | --- | --- |
343+
| `#!python name` | `#!python str` | The name of the channel to subscribe to. | N/A |
344+
| `#!python receiver` | `#!python AsyncMessageReceiver | None` | An async function that receives a `#!python message: dict` from the channel layer. If more than one receiver waits on the same channel, a random one will get the result (unless `#!python group=True` is defined). | `#!python None` |
345+
| `#!python group` | `#!python bool` | If `#!python True`, a "group channel" will be used. Messages sent within a group are broadcasted to all receivers on that channel. | `#!python False` |
346+
| `#!python layer` | `#!python str` | The channel layer to use. These layers must be defined in `#!python settings.py:CHANNEL_LAYERS`. | `#!python 'default'` |
347+
348+
<font size="4">**Returns**</font>
349+
350+
| Type | Description |
351+
| --- | --- |
352+
| `#!python AsyncMessageSender` | An async callable that can send a `#!python message: dict` |
353+
354+
??? warning "Extra Django configuration required"
355+
356+
In order to use this hook, you will need to configure Django to enable channel layers.
357+
358+
The [Django Channels documentation](https://channels.readthedocs.io/en/latest/topics/channel_layers.html#configuration) has information on what steps you need to take.
359+
360+
In summary, you will need to:
361+
362+
1. Run the following command to install `channels-redis` in your Python environment.
363+
364+
```bash linenums="0"
365+
pip install channels-redis
366+
```
367+
368+
2. Configure your `settings.py` to use `RedisChannelLayer` as your layer backend.
369+
370+
```python linenums="0"
371+
CHANNEL_LAYERS = {
372+
"default": {
373+
"BACKEND": "channels_redis.core.RedisChannelLayer",
374+
"CONFIG": {
375+
"hosts": [("127.0.0.1", 6379)],
376+
},
377+
},
378+
}
379+
```
380+
381+
??? question "How do I broadcast a message to multiple components?"
382+
383+
By default, if more than one receiver waits on the same channel, a random one will get the result.
384+
385+
However, by defining `#!python group=True` you can configure a "group channel", which will broadcast messages to all receivers.
386+
387+
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).
388+
389+
=== "components.py"
390+
391+
```python
392+
{% include "../../examples/python/use-channel-layer-group.py" %}
393+
```
394+
395+
??? question "How do I signal a re-render from something that isn't a component?"
396+
397+
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.
398+
399+
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.
400+
401+
In the example below, the sender will send a signal every time `#!python ExampleModel` is saved. Then, when the receiver component gets this signal, it explicitly calls `#!python set_message(...)` to trigger a re-render.
402+
403+
=== "components.py"
404+
405+
```python
406+
{% include "../../examples/python/use-channel-layer-signal-receiver.py" %}
407+
```
408+
=== "signals.py"
409+
410+
```python
411+
{% include "../../examples/python/use-channel-layer-signal-sender.py" %}
412+
```
413+
414+
---
415+
315416
## Connection Hooks
316417

317418
---

docs/src/reference/router.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<p class="intro" markdown>
44

5-
A variant of [`reactpy-router`](https://github.com/reactive-python/reactpy-router) that utilizes Django conventions.
5+
A Single Page Application URL router, which is a variant of [`reactpy-router`](https://github.com/reactive-python/reactpy-router) that uses Django conventions.
66

77
</p>
88

requirements/check-types.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
mypy
22
django-stubs[compatible-mypy]
3+
channels-redis

0 commit comments

Comments
 (0)