Skip to content

Commit a863ad3

Browse files
committed
wip use_query and use_mutation
1 parent e182a7e commit a863ad3

File tree

4 files changed

+195
-6
lines changed

4 files changed

+195
-6
lines changed

docs/features/hooks.md

+49-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,55 @@
44

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

7+
## Use Query and Use Mutation
8+
9+
<!-- TODO: Add description -->
10+
11+
```python
12+
from example_project.my_app.models import TodoItem
13+
from idom import component, html
14+
from django_idom.hooks import use_query, use_mutation
15+
16+
17+
def get_items():
18+
return TodoItem.objects.all()
19+
20+
def add_item(text: str):
21+
TodoItem(text=text).save()
22+
23+
24+
@component
25+
def todo_list():
26+
items_query = use_query(get_items)
27+
add_item_mutation = use_mutation(add_item, refetch=get_items)
28+
item_draft, set_item_draft = use_state("")
29+
30+
if items_query.loading:
31+
items_view = html.h2("Loading...")
32+
elif items_query.error:
33+
items_view = html.h2(f"Error when loading: {items.error}")
34+
else:
35+
items_view = html.ul(html.li(item, key=item) for item in items_query.data)
36+
37+
if add_item_mutation.loading:
38+
add_item_status = html.h2("Adding...")
39+
elif add_item_mutation.error:
40+
add_item_status = html.h2(f"Error when adding: {add_item_mutation.error}")
41+
else:
42+
add_item_status = ""
43+
44+
def handle_add_item(event):
45+
set_item_draft("")
46+
add_item_mutation.execute(text=item_draft)
47+
48+
return html.div(
49+
html.label("Add an item:")
50+
html.input({"type": "text", "onClick": handle_add_item})
51+
add_item_status,
52+
items_view,
53+
)
54+
```
55+
756
## Use Websocket
857

958
You can fetch the Django Channels websocket at any time by using `use_websocket`.
@@ -18,8 +67,6 @@ def MyComponent():
1867
return html.div(my_websocket)
1968
```
2069

21-
22-
2370
## Use Scope
2471

2572
This is a shortcut that returns the Websocket's `scope`.
@@ -34,7 +81,6 @@ def MyComponent():
3481
return html.div(my_scope)
3582
```
3683

37-
3884
## Use Location
3985

4086
??? info "This hook's behavior will be changed in a future update"

requirements/pkg-deps.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
channels >=3.0.0
22
idom >=0.39.0, <0.40.0
33
aiofile >=3.0
4+
typing_extensions

src/django_idom/hooks.py

+136-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,28 @@
1+
from __future__ import annotations
2+
13
from dataclasses import dataclass
2-
from typing import Awaitable, Callable, Dict, Optional, Type, Union
4+
from threading import Thread
5+
from types import FunctionType
6+
from typing import (
7+
Any,
8+
Awaitable,
9+
Callable,
10+
DefaultDict,
11+
Optional,
12+
Sequence,
13+
Type,
14+
Union,
15+
TypeVar,
16+
Generic,
17+
NamedTuple,
18+
)
19+
20+
from typing_extensions import ParamSpec
21+
from idom import use_callback
322

423
from idom.backend.types import Location
5-
from idom.core.hooks import Context, create_context, use_context
24+
from idom.core.hooks import Context, create_context, use_context, use_state, use_effect
25+
from django_idom.utils import UNDEFINED
626

727

828
@dataclass
@@ -26,7 +46,7 @@ def use_location() -> Location:
2646
return Location(scope["path"], f"?{search}" if search else "")
2747

2848

29-
def use_scope() -> Dict:
49+
def use_scope() -> dict[str, Any]:
3050
"""Get the current ASGI scope dictionary"""
3151
return use_websocket().scope
3252

@@ -37,3 +57,116 @@ def use_websocket() -> IdomWebsocket:
3757
if websocket is None:
3858
raise RuntimeError("No websocket. Are you running with a Django server?")
3959
return websocket
60+
61+
62+
_REFETCH_CALLBACKS: DefaultDict[FunctionType, set[Callable[[], None]]] = DefaultDict(
63+
set
64+
)
65+
66+
67+
_Data = TypeVar("_Data")
68+
_Params = ParamSpec("_Params")
69+
70+
71+
def use_query(
72+
query: Callable[_Params, _Data],
73+
*args: _Params.args,
74+
**kwargs: _Params.kwargs,
75+
) -> Query[_Data]:
76+
given_query = query
77+
query, _ = use_state(given_query)
78+
if given_query is not query:
79+
raise ValueError(f"Query function changed from {query} to {given_query}.")
80+
81+
data, set_data = use_state(UNDEFINED)
82+
loading, set_loading = use_state(True)
83+
error, set_error = use_state(None)
84+
85+
@use_callback
86+
def refetch() -> None:
87+
set_data(UNDEFINED)
88+
set_loading(True)
89+
set_error(None)
90+
91+
@use_effect(dependencies=[])
92+
def add_refetch_callback():
93+
# By tracking callbacks globally, any usage of the query function will be re-run
94+
# if the user has told a mutation to refetch it.
95+
_REFETCH_CALLBACKS[query].add(refetch)
96+
return lambda: _REFETCH_CALLBACKS[query].remove(refetch)
97+
98+
@use_effect(dependencies=None)
99+
def execute_query():
100+
if data is not UNDEFINED:
101+
return
102+
103+
def thread_target():
104+
try:
105+
returned = query(*args, **kwargs)
106+
except Exception as e:
107+
set_data(UNDEFINED)
108+
set_loading(False)
109+
set_error(e)
110+
else:
111+
set_data(returned)
112+
set_loading(False)
113+
set_error(None)
114+
115+
# We need to run this in a thread so we don't prevent rendering when loading.
116+
# I'm also hoping that Django is ok with this since this thread won't have an
117+
# active event loop.
118+
Thread(target=thread_target, daemon=True).start()
119+
120+
return Query(data, loading, error, refetch)
121+
122+
123+
class Query(NamedTuple, Generic[_Data]):
124+
data: _Data
125+
loading: bool
126+
error: Exception | None
127+
refetch: Callable[[], None]
128+
129+
130+
def use_mutation(
131+
mutate: Callable[_Params, None],
132+
refetch: Callable[..., Any] | Sequence[Callable[..., Any]],
133+
) -> Mutation[_Params]:
134+
loading, set_loading = use_state(True)
135+
error, set_error = use_state(None)
136+
137+
@use_callback
138+
def call(*args: _Params.args, **kwargs: _Params.kwargs) -> None:
139+
set_loading(True)
140+
141+
def thread_target():
142+
try:
143+
mutate(*args, **kwargs)
144+
except Exception as e:
145+
set_loading(False)
146+
set_error(e)
147+
else:
148+
set_loading(False)
149+
set_error(None)
150+
for query in (refetch,) if isinstance(refetch, Query) else refetch:
151+
refetch_callback = _REFETCH_CALLBACKS.get(query)
152+
if refetch_callback is not None:
153+
refetch_callback()
154+
155+
# We need to run this in a thread so we don't prevent rendering when loading.
156+
# I'm also hoping that Django is ok with this since this thread won't have an
157+
# active event loop.
158+
Thread(target=thread_target, daemon=True).start()
159+
160+
@use_callback
161+
def reset() -> None:
162+
set_loading(False)
163+
set_error(None)
164+
165+
return Query(call, loading, error, reset)
166+
167+
168+
class Mutation(NamedTuple, Generic[_Params]):
169+
execute: Callable[_Params, None]
170+
loading: bool
171+
error: Exception | None
172+
reset: Callable[[], None]

src/django_idom/utils.py

+9
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,12 @@ def _register_components(self, components: Set) -> None:
133133
"\033[0m",
134134
component,
135135
)
136+
137+
138+
class _Undefined:
139+
def __repr__(self):
140+
return "UNDEFINED"
141+
142+
143+
UNDEFINED = _Undefined()
144+
"""Sentinel for undefined values"""

0 commit comments

Comments
 (0)