Hooks
Overview¶
Prefabricated hooks can be used within your components.py
to help simplify development.
Note
Looking for standard React hooks?
This package only contains Django specific hooks. Standard hooks can be found within reactive-python/reactpy
.
Database Hooks¶
Use Query¶
Execute functions in the background and return the result, typically to read data from the Django ORM.
The default postprocessor expects your query function to return
a Django Model
or QuerySet
. This needs to be changed or disabled to execute other types of queries.
Query functions can be sync or async.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
1 2 3 4 5 |
|
See Interface
Parameters
Name | Type | Description | Default |
---|---|---|---|
query | Callable[FuncParams, Awaitable[Inferred]] | Callable[FuncParams, Inferred] | A function that executes a query and returns some data. | N/A |
kwargs | dict[str, Any] | None | Keyword arguments to passed into the query function. | None |
thread_sensitive | bool | Whether to run your query function in thread sensitive mode. This setting only applies to sync functions, and is turned on by default due to Django ORM limitations. | True |
postprocessor | AsyncPostprocessor | SyncPostprocessor | None | A callable that processes the query data before it is returned. The first argument of postprocessor function must be the query data . All proceeding arguments are optional postprocessor_kwargs . This postprocessor function must return the modified data . | None |
postprocessor_kwargs | dict[str, Any] | None | Keyworded arguments passed into the postprocessor function. | None |
Returns
Type | Description |
---|---|
Query[Inferred] | An object containing loading /error states, your data (if the query has successfully executed), and a refetch callable that can be used to re-run the query. |
How can I provide arguments to my query function?
kwargs
can be provided to your query function via the kwargs=...
parameter.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
How can I customize this hook's behavior?
This hook has several parameters that can be used to customize behavior.
Below are examples of values that can be modified.
thread_sensitive
Whether to run your synchronous query function in thread sensitive mode. Thread sensitive mode is turned on by default due to Django ORM limitations. See Django's sync_to_async
docs docs for more information.
This setting only applies to sync query functions, and will be ignored for async functions.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
postprocessor
By default, automatic recursive fetching of ManyToMany
or ForeignKey
fields is enabled within the django_query_postprocessor
. This is needed to prevent SynchronousOnlyOperation
exceptions when accessing these fields within your ReactPy components.
However, if you...
- Want to use this hook to defer IO intensive tasks to be computed in the background
- Want to to utilize
use_query
with a different ORM
... then you can either set a custom postprocessor
, or disable all postprocessing behavior by modifying the postprocessor=...
parameter. In the example below, we will set the postprocessor
to None
to disable postprocessing behavior.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
If you wish to create a custom postprocessor
, you will need to create a function where the first must be the query data
. All proceeding arguments are optional postprocessor_kwargs
(see below). This postprocessor
function must return the modified data
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
postprocessor_kwargs
By default, automatic recursive fetching of ManyToMany
or ForeignKey
fields is enabled within the django_query_postprocessor
. This is needed to prevent SynchronousOnlyOperation
exceptions when accessing these fields within your ReactPy components.
However, if you have deep nested trees of relational data, this may not be a desirable behavior. In these scenarios, you may prefer to manually fetch these relational fields using a second use_query
hook.
You can disable the prefetching behavior of the default postprocessor
(located at reactpy_django.utils.django_query_postprocessor
) via the postprocessor_kwargs=...
parameter.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
Note: In Django's ORM design, the field name to access foreign keys is postfixed with _set
by default.
Can I make ORM calls without hooks?
Due to Django's ORM design, database queries must be deferred using hooks. Otherwise, you will see a SynchronousOnlyOperation
exception.
These SynchronousOnlyOperation
exceptions may be removed in a future version of Django. However, it is best practice to always perform IO operations (such as ORM queries) via hooks to prevent performance issues.
Can I make a failed query try again?
Yes, use_mutation
can be re-executed by calling reset()
on your use_mutation
instance.
For example, take a look at reset_event
below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
1 2 3 4 5 |
|
Why does the example query function return TodoItem.objects.all()
?
This design decision was based on Apollo's useQuery
hook, but ultimately helps avoid Django's SynchronousOnlyOperation
exceptions.
With the Model
or QuerySet
your function returns, this hook uses the default postprocessor to ensure that all deferred or lazy fields are executed.
Use Mutation¶
Modify data in the background, typically to create/update/delete data from the Django ORM.
Mutation functions can return False
to manually prevent your refetch=...
function from executing. All other returns are ignored.
Mutation functions can be sync or async.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
1 2 3 4 5 |
|
See Interface
Parameters
Name | Type | Description | Default |
---|---|---|---|
mutation | Callable[FuncParams, bool | None] | Callable[FuncParams, Awaitable[bool | None]] | A callable that performs Django ORM create, update, or delete functionality. If this function returns False , then your refetch function will not be used. | N/A |
thread_sensitive | bool | Whether to run the mutation in thread sensitive mode. This setting only applies to sync functions, and is turned on by default due to Django ORM limitations. | True |
refetch | Callable[..., Any] | Sequence[Callable[..., Any]] | None | A query function (the function you provide to your use_query hook) or a sequence of query functions that need a refetch if the mutation succeeds. This is useful for refreshing data after a mutation has been performed. | None |
Returns
Type | Description |
---|---|
Mutation[FuncParams] | An object containing loading /error states, and a reset callable that will set loading /error states to defaults. This object can be called to run the query. |
How can I provide arguments to my mutation function?
*args
and **kwargs
can be provided to your mutation function via mutation(...)
parameters.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
How can I customize this hook's behavior?
This hook has several parameters that can be used to customize behavior.
Below are examples of values that can be modified.
thread_sensitive
Whether to run your synchronous mutation function in thread sensitive mode. Thread sensitive mode is turned on by default due to Django ORM limitations. See Django's sync_to_async
docs docs for more information.
This setting only applies to sync query functions, and will be ignored for async functions.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
Can I make ORM calls without hooks?
Due to Django's ORM design, database queries must be deferred using hooks. Otherwise, you will see a SynchronousOnlyOperation
exception.
These SynchronousOnlyOperation
exceptions may be removed in a future version of Django. However, it is best practice to always perform IO operations (such as ORM queries) via hooks to prevent performance issues.
Can I make a failed mutation try again?
Yes, use_mutation
can be re-executed by calling reset()
on your use_mutation
instance.
For example, take a look at reset_event
below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
1 2 3 4 5 |
|
Can use_mutation
trigger a refetch of use_query
?
Yes, use_mutation
can queue a refetch of a use_query
via the refetch=...
argument.
The example below is a merge of the use_query
and use_mutation
examples above with the addition of a use_mutation(refetch=...)
argument.
Please note that refetch
will cause all use_query
hooks that use get_items
in the current component tree will be refetched.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
|
1 2 3 4 5 |
|
User Hooks¶
Use Auth¶
Provides a NamedTuple
containing async login
and async logout
functions.
This hook utilizes the Django's authentication framework in a way that provides persistent login.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
See Interface
Parameters
None
Returns
Type | Description |
---|---|
UseAuthTuple | A named tuple containing login and logout async functions. |
Extra Django configuration required
Your ReactPy WebSocket must utilize AuthMiddlewareStack
in order to use this hook.
from channels.auth import AuthMiddlewareStack # noqa: E402
application = ProtocolTypeRouter({
"http": django_asgi_app,
"websocket": AuthMiddlewareStack(URLRouter([REACTPY_WEBSOCKET_ROUTE])),
})
Why use this instead of channels.auth.login
?
The channels.auth.*
functions cannot trigger re-renders of your ReactPy components. Additionally, they do not provide persistent authentication when used within ReactPy.
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.
To work around this limitation, when use_auth().login()
is called within your application, ReactPy performs the following process...
- The server authenticates the user into the WebSocket
- The server generates a temporary login token
- The server commands the browser to use the login token (via HTTP)
- The client performs the HTTP request
- The server returns the HTTP response, which contains all necessary cookies
- The client stores these cookies in the browser
This ultimately results in persistent authentication which will be retained even if the browser tab is refreshed.
Use User¶
Shortcut that returns the WebSocket or HTTP connection's User
.
1 2 3 4 5 6 7 8 9 10 |
|
See Interface
Parameters
None
Returns
Type | Description |
---|---|
AbstractUser | A Django User , which can also be an AnonymousUser . |
Use User Data¶
Store or retrieve a dict
containing arbitrary data specific to the connection's User
.
This hook is useful for storing user-specific data, such as preferences or settings.
User data saved with this hook is stored within the REACTPY_DATABASE
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
See Interface
Parameters
Name | Type | Description | Default |
---|---|---|---|
default_data | None | dict[str, Callable[[], Any] | Callable[[], Awaitable[Any]] | Any] | A dictionary containing {key: default_value} pairs. For computationally intensive defaults, your default_value can be sync or async functions that return the value to set. | None |
save_default_data | bool | If True , default_data values will automatically be stored within the database if they do not exist. | False |
Returns
Type | Description |
---|---|
UserData | A NamedTuple containing a Query and Mutation objects used to access/modify user data. Read the use_query and use_mutation docs for more details. |
How do I set default values?
You can configure default user data via the default_data
parameter.
This parameter accepts a dictionary containing a {key: default_value}
pairs. For computationally intensive defaults, your default_value
can be sync or async functions that return the value to set.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
Communication Hooks¶
Use Channel Layer¶
Subscribe to a Django Channels Layer to communicate messages.
Layers are a multiprocessing-safe communication system that allows you to send/receive messages between different parts of your application.
This is often used to create chat systems, synchronize data between components, or signal re-renders from outside your components.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
See Interface
Parameters
Name | Type | Description | Default |
---|---|---|---|
channel | str | None | The name of the channel this hook will send/receive messages on. If group is defined and channel is None , ReactPy will automatically generate a unique channel name. | None |
group | str | None | If configured, the channel is added to a group and any messages sent by AsyncMessageSender is broadcasted to all channels within the group . | None |
receiver | AsyncMessageReceiver | None | An async function that receives a message: dict from a channel. | None |
layer | str | The Django Channels layer to use. This layer must be defined in settings.py:CHANNEL_LAYERS . | 'default' |
Returns
Type | Description |
---|---|
AsyncMessageSender | An async callable that can send messages to the channel(s). This callable accepts a single argument, message: dict , which is the data sent to the channel or group of channels. |
Extra Django configuration required
In order to use this hook, you will need to configure Django to enable channel layers.
The Django Channels documentation has information on what steps you need to take.
Here is a short summary of the most common installation steps:
-
Install
redis
on your machine. -
Install
channels-redis
in your Python environment.pip install channels-redis
-
Configure your
settings.py
to useRedisChannelLayer
as your layer backend.CHANNEL_LAYERS = { "default": { "BACKEND": "channels_redis.core.RedisChannelLayer", "CONFIG": { "hosts": [("127.0.0.1", 6379)], }, }, }
Learn about the quirks of Django Channel Layers
ReactPy tries to simplify the process of using Django Channels Layers, but it is important to understand how they work.
There are a few quirks of Django Channels Layers to be aware of:
- Any given
channel
should only have onereceiver
registered to it, under normal circumstances.- This is why ReactPy automatically generates a unique channel name when using
group
.- When using
group
within this hook, it is suggested to leavechannel
undefined to let ReactPy automatically create a unique channel name (unless you know what you are doing).
- When using
- If you have multiple receivers for the same
channel
, only one receiver will get the result.- This quirk extends to groups as well. For example, If you have two component instances that use the same
channel
within agroup
, the message will only reach one receiver (for that channel).
- This quirk extends to groups as well. For example, If you have two component instances that use the same
- This is why ReactPy automatically generates a unique channel name when using
- Channels exist independently of their
group
.- Groups are just a loose collection of channel names where a copy of each message can be sent.
- As a result, Django allows you to send messages directly to a
channel
even if it is within agroup
.
- By default,
RedisChannelLayer
will close groups once they have existed for more than 24 hours.- You need to create your own subclass of
RedisChannelLayer
to change this behavior.
- You need to create your own subclass of
- By default,
RedisChannelLayer
only allows 100 messages backlogged within achannel
receive queue.- Rapidly sending messages can overwhelm this queue, resulting in new messages being dropped.
- If you expect to exceed this limit, you need to create your own subclass of
RedisChannelLayer
to change this behavior.
How do I broadcast a message to multiple components?
Groups allow you to broadcast messages to all channels within that group. If you do not define a channel
while using groups, ReactPy will automatically generate a unique channel name for you.
In the example below, since all components use the same channel group
, messages sent by my_sender_component
will reach all existing instances of my_receiver_component_1
and my_receiver_component_2
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
|
How do I send a message to a single component (point-to-point communication)?
The most common way of using use_channel_layer
is to broadcast messages to multiple components via a group
.
However, you can also use this hook to establish unidirectional, point-to-point communication towards a single receiver
function. This is slightly more efficient since it avoids the overhead of group
broadcasting.
In the example below, my_sender_component
will communicate directly to a single instance of my_receiver_component
. This is achieved by defining a channel
while omitting the group
parameter.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
Note that if you have multiple instances of my_receiver_component
using the same channel
, only one will receive the message.
How do I signal a re-render from something that isn't a component?
There are occasions where you may want to signal to the use_channel_layer
hook from something that isn't a component, such as a Django model signal.
In these cases, you can use the use_channel_layer
hook to receive a signal within your component, and then use the get_channel_layer().send(...)
to send the signal.
In the example below, the sender will signal every time ExampleModel
is saved. Then, when the receiver gets this signal, it explicitly calls set_message(...)
to trigger a re-render.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Connection Hooks¶
Use Connection¶
Returns the active connection, which is either a Django WebSocket or a HTTP Request.
1 2 3 4 5 6 7 8 9 10 |
|
See Interface
Parameters
None
Returns
Type | Description |
---|---|
Connection | An object that contains a carrier (WebSocket or HttpRequest ), scope , and location . |
Use Scope¶
Shortcut that returns the WebSocket or HTTP connection's scope.
1 2 3 4 5 6 7 8 9 10 |
|
See Interface
Parameters
None
Returns
Type | Description |
---|---|
MutableMapping[str, Any] | The connection's scope . |
Use Location¶
Shortcut that returns the browser's current Location
.
1 2 3 4 5 6 7 8 9 10 |
|
See Interface
Parameters
None
Returns
Type | Description |
---|---|
Location | An object containing the current URL's pathname and search query. |
Use Origin¶
Shortcut that returns the WebSocket or HTTP connection's origin
.
You can expect this hook to provide strings such as http://example.com
.
1 2 3 4 5 6 7 8 9 10 |
|
See Interface
Parameters
None
Returns
Type | Description |
---|---|
str | None | A string containing the browser's current origin, obtained from WebSocket or HTTP headers (if available). |
Use Root ID¶
Shortcut that returns the root component's id
from the WebSocket or HTTP connection.
The root ID is a randomly generated uuid4
. It is notable to mention that it is persistent across the current connection. The uuid
is reset only when the page is refreshed.
This is useful when used in combination with use_channel_layer
to send messages to a specific component instance.
1 2 3 4 5 6 7 8 9 10 |
|
See Interface
Parameters
None
Returns
Type | Description |
---|---|
str | A string containing the root component's id . |
Use Re-render¶
Returns a function that can be used to trigger a re-render of the entire component tree.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
See Interface
Parameters
None
Returns
Type | Description |
---|---|
Callable[[], None] | A function that triggers a re-render of the entire component tree. |