1
+ from __future__ import annotations
2
+
1
3
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
3
22
4
23
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
6
26
7
27
8
28
@dataclass
@@ -26,7 +46,7 @@ def use_location() -> Location:
26
46
return Location (scope ["path" ], f"?{ search } " if search else "" )
27
47
28
48
29
- def use_scope () -> Dict :
49
+ def use_scope () -> dict [ str , Any ] :
30
50
"""Get the current ASGI scope dictionary"""
31
51
return use_websocket ().scope
32
52
@@ -37,3 +57,116 @@ def use_websocket() -> IdomWebsocket:
37
57
if websocket is None :
38
58
raise RuntimeError ("No websocket. Are you running with a Django server?" )
39
59
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 ]
0 commit comments