Skip to content

Commit c01152a

Browse files
committed
wip use_query and use_mutation
1 parent 1e571ff commit c01152a

File tree

4 files changed

+199
-3
lines changed

4 files changed

+199
-3
lines changed

docs/features/hooks.md

+49
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,55 @@
22

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

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

756
You can fetch the Django Channels websocket at any time by using `use_websocket`.

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

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

326
from idom.backend.types import Location
4-
from idom.core.hooks import Context, create_context, use_context
27+
from idom.core.hooks import Context, create_context, use_context, use_state, use_effect
28+
from django_idom.utils import UNDEFINED
529

630
from django_idom.types import IdomWebsocket
731

@@ -19,7 +43,7 @@ def use_location() -> Location:
1943
return Location(scope["path"], f"?{search}" if search else "")
2044

2145

22-
def use_scope() -> Dict:
46+
def use_scope() -> dict[str, Any]:
2347
"""Get the current ASGI scope dictionary"""
2448
return use_websocket().scope
2549

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