Skip to content

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.


Use Query

This hook is used read data from the Django ORM.

The query function you provide must return either a Model or QuerySet.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from example.models import TodoItem
from reactpy import component, html
from reactpy_django.hooks import use_query


def get_items():
    return TodoItem.objects.all()


@component
def todo_list():
    item_query = use_query(get_items)

    if item_query.loading:
        rendered_items = html.h2("Loading...")
    elif item_query.error or not item_query.data:
        rendered_items = html.h2("Error when loading!")
    else:
        rendered_items = html.ul([html.li(item, key=item) for item in item_query.data])

    return html.div("Rendered items: ", rendered_items)
1
2
3
4
5
from django.db.models import CharField, Model


class TodoItem(Model):
    text: CharField = CharField(max_length=255)
See Interface

Parameters

Name Type Description Default
options QueryOptions | None An optional QueryOptions object that can modify how the query is executed. None
query Callable[_Params, _Result | None] A callable that returns a Django Model or QuerySet. N/A
*args _Params.args Positional arguments to pass into query. N/A
**kwargs _Params.kwargs Keyword arguments to pass into query. N/A

Returns

Type Description
Query[_Result | None] 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?

*args and **kwargs can be provided to your query function via use_query parameters.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from reactpy import component
from reactpy_django.hooks import use_query


def example_query(value: int, other_value: bool = False):
    ...


@component
def my_component():
    query = use_query(
        example_query,
        123,
        other_value=True,
    )

    return str(query.data)
Why does get_items in the example return TodoItem.objects.all()?

This was a technical design decision to based on Apollo's useQuery hook, but ultimately helps avoid Django's SynchronousOnlyOperation exceptions.

The use_query hook ensures the provided Model or QuerySet executes all deferred/lazy queries safely prior to reaching your components.

How can I use QueryOptions to customize fetching behavior?

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
18
19
20
21
from reactpy import component
from reactpy_django.hooks import use_query
from reactpy_django.types import QueryOptions


def execute_thread_safe_operation():
    """This is an example query function that does some thread-safe operation."""
    pass


@component
def my_component():
    query = use_query(
        QueryOptions(thread_sensitive=False),
        execute_thread_safe_operation,
    )

    if query.loading or query.error:
        return None

    return str(query.data)

postprocessor

By default, automatic recursive fetching of ManyToMany or ForeignKey fields is enabled within the default QueryOptions.postprocessor. This is needed to prevent SynchronousOnlyOperation exceptions when accessing these fields within your ReactPy components.

However, if you...

  1. Want to use this hook to defer IO intensive tasks to be computed in the background
  2. 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 QueryOptions.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
21
from reactpy import component
from reactpy_django.hooks import use_query
from reactpy_django.types import QueryOptions


def execute_io_intensive_operation():
    """This is an example query function that does something IO intensive."""
    pass


@component
def my_component():
    query = use_query(
        QueryOptions(postprocessor=None),
        execute_io_intensive_operation,
    )

    if query.loading or query.error:
        return None

    return str(query.data)

If you wish to create a custom postprocessor, you will need to create a callable.

The first argument of postprocessor must be the query data. All proceeding arguments are optional postprocessor_kwargs (see below). This postprocessor 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
29
30
31
from reactpy import component
from reactpy_django.hooks import use_query
from reactpy_django.types import QueryOptions


def my_postprocessor(data, example_kwarg=True):
    if example_kwarg:
        return data

    return dict(data)


def execute_io_intensive_operation():
    """This is an example query function that does something IO intensive."""
    pass


@component
def my_component():
    query = use_query(
        QueryOptions(
            postprocessor=my_postprocessor,
            postprocessor_kwargs={"example_kwarg": False},
        ),
        execute_io_intensive_operation,
    )

    if query.loading or query.error:
        return None

    return str(query.data)

postprocessor_kwargs

By default, automatic recursive fetching of ManyToMany or ForeignKey fields is enabled within the default QueryOptions.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 QueryOptions.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
30
31
from example.models import TodoItem
from reactpy import component
from reactpy_django.hooks import use_query
from reactpy_django.types import QueryOptions


def get_model_with_relationships():
    """This is an example query function that gets `MyModel` which has a ManyToMany field, and
    additionally other models that have formed a ForeignKey association to `MyModel`.

    ManyToMany Field: `many_to_many_field`
    ForeignKey Field: `foreign_key_field_set`
    """
    return TodoItem.objects.get(id=1)


@component
def my_component():
    query = use_query(
        QueryOptions(
            postprocessor_kwargs={"many_to_many": False, "many_to_one": False}
        ),
        get_model_with_relationships,
    )

    if query.loading or query.error or not query.data:
        return None

    # By disabling `many_to_many` and `many_to_one`, accessing these fields will now
    # generate a `SynchronousOnlyOperation` exception
    return f"{query.data.many_to_many_field} {query.data.foriegn_key_field_set}"

Note: In Django's ORM design, the field name to access foreign keys is postfixed with _set by default.

Can I define async query functions?

Async functions are supported by use_query. You can use them in the same way as a sync query function.

However, be mindful of Django async ORM restrictions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from channels.db import database_sync_to_async
from example.models import TodoItem
from reactpy import component, html
from reactpy_django.hooks import use_query


async def get_items():
    return await database_sync_to_async(TodoItem.objects.all)()


@component
def todo_list():
    item_query = use_query(get_items)

    if item_query.loading:
        rendered_items = html.h2("Loading...")
    elif item_query.error or not item_query.data:
        rendered_items = html.h2("Error when loading!")
    else:
        rendered_items = html.ul([html.li(item, key=item) for item in item_query.data])

    return html.div("Rendered items: ", rendered_items)
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 resolved in a future version of Django containing an asynchronous ORM. However, it is best practice to always perform ORM calls in the background via hooks.

Use Mutation

This hook is used to create, update, or delete Django ORM objects.

The mutation function you provide should have no return value.

 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
from example.models import TodoItem
from reactpy import component, html
from reactpy_django.hooks import use_mutation


def add_item(text: str):
    TodoItem(text=text).save()


@component
def todo_list():
    item_mutation = use_mutation(add_item)

    def submit_event(event):
        if event["key"] == "Enter":
            item_mutation.execute(text=event["target"]["value"])

    if item_mutation.loading:
        mutation_status = html.h2("Adding...")
    elif item_mutation.error:
        mutation_status = html.h2("Error when adding!")
    else:
        mutation_status = html.h2("Mutation done.")

    return html.div(
        html.label("Add an item:"),
        html.input({"type": "text", "onKeyDown": submit_event}),
        mutation_status,
    )
1
2
3
4
5
from django.db.models import CharField, Model


class TodoItem(Model):
    text: CharField = CharField(max_length=255)
See Interface

Parameters

Name Type Description Default
mutate Callable[_Params, 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
refetch Callable[..., Any] | Sequence[Callable[..., Any]] | None A query function (used by the use_query hook) or a sequence of query functions that will be called if the mutation succeeds. This is useful for refetching data after a mutation has been performed. None

Returns

Type Description
Mutation[_Params] An object containing loading/error states, a reset callable that will set loading/error states to defaults, and a execute callable that will run the query.
How can I provide arguments to my mutation function?

*args and **kwargs can be provided to your mutation function via #!python mutation.execute` parameters.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from reactpy import component
from reactpy_django.hooks import use_mutation


def example_mutation(value: int, other_value: bool = False):
    ...


@component
def my_component():
    mutation = use_mutation(example_mutation)

    mutation.execute(123, other_value=True)

    ...
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 any use_query hooks that use get_items will be refetched upon a successful mutation.

 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
from example.models import TodoItem
from reactpy import component, html
from reactpy_django.hooks import use_mutation, use_query


def get_items():
    return TodoItem.objects.all()


def add_item(text: str):
    TodoItem(text=text).save()


@component
def todo_list():
    item_query = use_query(get_items)
    item_mutation = use_mutation(add_item, refetch=get_items)

    def submit_event(event):
        if event["key"] == "Enter":
            item_mutation.execute(text=event["target"]["value"])

    # Handle all possible query states
    if item_query.loading:
        rendered_items = html.h2("Loading...")
    elif item_query.error or not item_query.data:
        rendered_items = html.h2("Error when loading!")
    else:
        rendered_items = html.ul(html.li(item, key=item) for item in item_query.data)

    # Handle all possible mutation states
    if item_mutation.loading:
        mutation_status = html.h2("Adding...")
    elif item_mutation.error:
        mutation_status = html.h2("Error when adding!")
    else:
        mutation_status = html.h2("Mutation done.")

    return html.div(
        html.label("Add an item:"),
        html.input({"type": "text", "onKeyDown": submit_event}),
        mutation_status,
        rendered_items,
    )
1
2
3
4
5
from django.db.models import CharField, Model


class TodoItem(Model):
    text: CharField = CharField(max_length=255)
Can I make a failed use_mutation try again?

Yes, a use_mutation can be re-performed 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
from example.models import TodoItem
from reactpy import component, html
from reactpy_django.hooks import use_mutation


def add_item(text: str):
    TodoItem(text=text).save()


@component
def todo_list():
    item_mutation = use_mutation(add_item)

    def reset_event(event):
        item_mutation.reset()

    def submit_event(event):
        if event["key"] == "Enter":
            item_mutation.execute(text=event["target"]["value"])

    if item_mutation.loading:
        mutation_status = html.h2("Adding...")
    elif item_mutation.error:
        mutation_status = html.button({"onClick": reset_event}, "Error: Try again!")
    else:
        mutation_status = html.h2("Mutation done.")

    return html.div(
        html.label("Add an item:"),
        html.input({"type": "text", "onKeyDown": submit_event}),
        mutation_status,
    )
1
2
3
4
5
from django.db.models import CharField, Model


class TodoItem(Model):
    text: CharField = CharField(max_length=255)
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 resolved in a future version of Django containing an asynchronous ORM. However, it is best practice to always perform ORM calls in the background via hooks.

Use Connection

This hook is used to fetch the Django Channels WebSocket.

1
2
3
4
5
6
7
8
from reactpy import component, html
from reactpy_django.hooks import use_connection


@component
def my_component():
    my_connection = use_connection()
    return html.div(str(my_connection))
See Interface

Parameters

None

Returns

Type Description
Connection The component's WebSocket.

Use Scope

This is a shortcut that returns the WebSocket's scope.

1
2
3
4
5
6
7
8
from reactpy import component, html
from reactpy_django.hooks import use_scope


@component
def my_component():
    my_scope = use_scope()
    return html.div(str(my_scope))
See Interface

Parameters

None

Returns

Type Description
MutableMapping[str, Any] The WebSocket's scope.

Use Location

This is a shortcut that returns the WebSocket's path.

You can expect this hook to provide strings such as /reactpy/my_path.

1
2
3
4
5
6
7
8
from reactpy import component, html
from reactpy_django.hooks import use_location


@component
def my_component():
    my_location = use_location()
    return html.div(str(my_location))
See Interface

Parameters

None

Returns

Type Description
Location An object containing the current URL's pathname and search query.
This hook's behavior will be changed in a future update

This hook will be updated to return the browser's currently active HTTP path. This change will come in alongside ReactPy URL routing support.

Check out reactive-python/reactpy-django#147 for more information.

Use Origin

This is a shortcut that returns the WebSocket's origin.

You can expect this hook to provide strings such as http://example.com.

1
2
3
4
5
6
7
8
from reactpy import component, html
from reactpy_django.hooks import use_origin


@component
def my_component():
    my_origin = use_origin()
    return html.div(my_origin or "No origin")
See Interface

Parameters

None

Returns

Type Description
str | None A string containing the browser's current origin, obtained from WebSocket headers (if available).

Last update: September 7, 2023
Authors: Mark Bakhit