4
4
useDashboard ,
5
5
useIsWorkspaceActionsEnabled ,
6
6
} from "components/Dashboard/DashboardProvider" ;
7
- import { FC , useEffect , useState } from "react" ;
7
+ import { type FC , useEffect , useState , useSyncExternalStore } from "react" ;
8
8
import { Helmet } from "react-helmet-async" ;
9
9
import { pageTitle } from "utils/page" ;
10
10
import { useWorkspacesData , useWorkspaceUpdate } from "./data" ;
@@ -21,15 +21,34 @@ import { MONOSPACE_FONT_FAMILY } from "theme/constants";
21
21
import TextField from "@mui/material/TextField" ;
22
22
import { displayError } from "components/GlobalSnackbar/utils" ;
23
23
import { getErrorMessage } from "api/errors" ;
24
+ import { useEffectEvent } from "hooks/hookPolyfills" ;
25
+
26
+ function useSafeSearchParams ( ) {
27
+ // Have to wrap setSearchParams because React Router doesn't make sure that
28
+ // the function's memory reference stays stable on each render, even though
29
+ // its logic never changes, and even though it has function update support
30
+ const [ searchParams , setSearchParams ] = useSearchParams ( ) ;
31
+ const stableSetSearchParams = useEffectEvent ( setSearchParams ) ;
32
+
33
+ // Need this to be a tuple type, but can't use "as const", because that would
34
+ // make the whole array readonly and cause type mismatches downstream
35
+ return [ searchParams , stableSetSearchParams ] as ReturnType <
36
+ typeof useSearchParams
37
+ > ;
38
+ }
24
39
25
40
const WorkspacesPage : FC = ( ) => {
26
41
const [ dormantWorkspaces , setDormantWorkspaces ] = useState < Workspace [ ] > ( [ ] ) ;
27
42
// If we use a useSearchParams for each hook, the values will not be in sync.
28
43
// So we have to use a single one, centralizing the values, and pass it to
29
44
// each hook.
30
- const searchParamsResult = useSearchParams ( ) ;
45
+ const searchParamsResult = useSafeSearchParams ( ) ;
31
46
const pagination = usePagination ( { searchParamsResult } ) ;
32
- const filterProps = useWorkspacesFilter ( { searchParamsResult, pagination } ) ;
47
+ const filterProps = useWorkspacesFilter ( {
48
+ searchParamsResult,
49
+ onFilterChange : ( ) => pagination . goToPage ( 1 ) ,
50
+ } ) ;
51
+
33
52
const { data, error, queryKey, refetch } = useWorkspacesData ( {
34
53
...pagination ,
35
54
query : filterProps . filter . query ,
@@ -121,20 +140,58 @@ const WorkspacesPage: FC = () => {
121
140
122
141
export default WorkspacesPage ;
123
142
143
+ const workspaceFilterKey = "WorkspacesPage/filter" ;
144
+ const defaultWorkspaceFilter = "owner:me" ;
145
+
146
+ // Function should stay outside components as much as possible; if declared
147
+ // inside the component, React would add/remove event listeners every render
148
+ function subscribeToFilterChanges ( notifyReact : ( ) => void ) {
149
+ const onStorageChange = ( event : StorageEvent ) => {
150
+ const { key, storageArea, oldValue, newValue } = event ;
151
+
152
+ const shouldNotify =
153
+ key === workspaceFilterKey &&
154
+ storageArea === window . localStorage &&
155
+ newValue !== oldValue ;
156
+
157
+ if ( shouldNotify ) {
158
+ notifyReact ( ) ;
159
+ }
160
+ } ;
161
+
162
+ window . addEventListener ( "storage" , onStorageChange ) ;
163
+ return ( ) => window . removeEventListener ( "storage" , onStorageChange ) ;
164
+ }
165
+
124
166
type UseWorkspacesFilterOptions = {
125
167
searchParamsResult : ReturnType < typeof useSearchParams > ;
126
- pagination : ReturnType < typeof usePagination > ;
168
+ onFilterChange : ( ) => void ;
127
169
} ;
128
170
129
171
const useWorkspacesFilter = ( {
130
172
searchParamsResult,
131
- pagination ,
173
+ onFilterChange ,
132
174
} : UseWorkspacesFilterOptions ) => {
175
+ // Using useSyncExternalStore store to safely access localStorage from the
176
+ // first render; both snapshot callbacks return primitives, so no special
177
+ // trickery needed to prevent hook from immediately blowing up in dev mode
178
+ const localStorageFilter = useSyncExternalStore (
179
+ subscribeToFilterChanges ,
180
+ ( ) => {
181
+ return (
182
+ window . localStorage . getItem ( workspaceFilterKey ) ??
183
+ defaultWorkspaceFilter
184
+ ) ;
185
+ } ,
186
+ ( ) => defaultWorkspaceFilter ,
187
+ ) ;
188
+
133
189
const filter = useFilter ( {
134
- initialValue : `owner:me` ,
190
+ fallbackFilter : localStorageFilter ,
135
191
searchParamsResult,
136
- onUpdate : ( ) => {
137
- pagination . goToPage ( 1 ) ;
192
+ onUpdate : ( newValues ) => {
193
+ window . localStorage . setItem ( workspaceFilterKey , newValues ) ;
194
+ onFilterChange ( ) ;
138
195
} ,
139
196
} ) ;
140
197
0 commit comments