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
2
25
3
26
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
5
29
6
30
from django_idom .types import IdomWebsocket
7
31
@@ -19,7 +43,7 @@ def use_location() -> Location:
19
43
return Location (scope ["path" ], f"?{ search } " if search else "" )
20
44
21
45
22
- def use_scope () -> Dict :
46
+ def use_scope () -> dict [ str , Any ] :
23
47
"""Get the current ASGI scope dictionary"""
24
48
return use_websocket ().scope
25
49
@@ -30,3 +54,116 @@ def use_websocket() -> IdomWebsocket:
30
54
if websocket is None :
31
55
raise RuntimeError ("No websocket. Are you running with a Django server?" )
32
56
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 ]
0 commit comments