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
326from 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
630from 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 ]
0 commit comments