Skip to content

use_sync_to_async Hook #84

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 34 additions & 5 deletions docs/features/hooks.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,41 @@
# Django Hooks

???+ tip "Looking for more hooks?"

Check out the [IDOM Core docs](https://idom-docs.herokuapp.com/docs/reference/hooks-api.html?highlight=hooks) on hooks!

## Use Sync to Async

This is the suggested method of performing ORM queries when using Django IDOM.

Fundamentally, this hook is an ORM-safe version of [use_effect](https://idom-docs.herokuapp.com/docs/reference/hooks-api.html#use-effect).

```python title="components.py"
from example_project.my_app.models import Category
from channels.db import database_sync_to_async
from idom import component, html
from django_idom import hooks

@component
def simple_list():
categories, set_categories = hooks.use_state(None)

@hooks.use_sync_to_async
def get_categories():
if categories:
return
set_categories(Category.objects.all())

if not categories:
return html.h2("Loading...")
Comment on lines +27 to +28
Copy link
Contributor

@rmorshea rmorshea Jul 2, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Part of the reason for the data, loading, error pattern is that the if not categories check does not actually indicate whether the query is loading - it could be that there are no categories to show, that loading categories failed, or that categories are indeed loading.

You also need a convenient way to re-execute queries that are related. For example, triggering an add_categories database operation should cause the get_categories effect to re-run. Not only that, but other components that loaded data from the Categories model might need to update as well.

While this PR does allow people to use the ORM within their components, I don't think it makes it convenient to handle all the edge cases. I've drafted an alternative (#86) that follows in the style of Apollo.


return html.ul(
[html.li(category.name, key=category.name) for category in categories]
)
```

??? question "Why can't 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.

## Use Websocket

You can fetch the Django Channels websocket at any time by using `use_websocket`.
Expand All @@ -18,8 +50,6 @@ def MyComponent():
return html.div(my_websocket)
```



## Use Scope

This is a shortcut that returns the Websocket's `scope`.
Expand All @@ -34,7 +64,6 @@ def MyComponent():
return html.div(my_scope)
```


## Use Location

??? info "This hook's behavior will be changed in a future update"
Expand Down
72 changes: 70 additions & 2 deletions src/django_idom/hooks.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,32 @@
from dataclasses import dataclass
from typing import Awaitable, Callable, Dict, Optional, Type, Union
from inspect import iscoroutinefunction
from typing import (
TYPE_CHECKING,
Any,
Awaitable,
Callable,
Dict,
Optional,
Sequence,
Type,
Union,
overload,
)

from channels.db import database_sync_to_async
from idom.backend.types import Location
from idom.core.hooks import Context, create_context, use_context
from idom.core.hooks import (
Context,
_EffectApplyFunc,
create_context,
use_context,
use_effect,
)


if not TYPE_CHECKING:
# make flake8 think that this variable exists
ellipsis = type(...)


@dataclass
Expand Down Expand Up @@ -37,3 +61,47 @@ def use_websocket() -> IdomWebsocket:
if websocket is None:
raise RuntimeError("No websocket. Are you running with a Django server?")
return websocket


@overload
def use_sync_to_async(
function: None = None,
dependencies: Union[Sequence[Any], ellipsis, None] = ...,
) -> Callable[[_EffectApplyFunc], None]:
...


@overload
def use_sync_to_async(
function: _EffectApplyFunc,
dependencies: Union[Sequence[Any], ellipsis, None] = ...,
) -> None:
...


def use_sync_to_async(
function: Optional[_EffectApplyFunc] = None,
dependencies: Union[Sequence[Any], ellipsis, None] = ...,
) -> Optional[Callable[[_EffectApplyFunc], None]]:
"""This is a sync_to_async wrapper for `idom.hooks.use_effect`.
See the full :ref:`Use Effect` docs for details

Parameters:
function:
Applies the effect and can return a clean-up function
dependencies:
Dependencies for the effect. The effect will only trigger if the identity
of any value in the given sequence changes (i.e. their :func:`id` is
different). By default these are inferred based on local variables that are
referenced by the given function.

Returns:
If a function is not provided:
A decorator.
Otherwise:
`None`
"""
if function and iscoroutinefunction(function):
raise ValueError("use_sync_to_async cannot be used with async functions")
sync_to_async_function = database_sync_to_async(function) if function else None
return use_effect(sync_to_async_function, dependencies)