1+ from __future__ import annotations
2+
13from 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
423from 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 ]
0 commit comments