diff --git a/docs/features/hooks.md b/docs/features/hooks.md index e1cc4d04..8318ee87 100644 --- a/docs/features/hooks.md +++ b/docs/features/hooks.md @@ -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...") + + 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`. @@ -18,8 +50,6 @@ def MyComponent(): return html.div(my_websocket) ``` - - ## Use Scope This is a shortcut that returns the Websocket's `scope`. @@ -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" diff --git a/src/django_idom/hooks.py b/src/django_idom/hooks.py index 5d088223..cb58dadb 100644 --- a/src/django_idom/hooks.py +++ b/src/django_idom/hooks.py @@ -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 @@ -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)