1
+ diff --git var/folders/kt/zd3bfncd0c3gjx25hbcq483c0000gn/T/epicshop/diff/advanced-react-apis/09.03.solution/dn2ncwjsbmo/index.test.ts var/folders/kt/zd3bfncd0c3gjx25hbcq483c0000gn/T/epicshop/diff/advanced-react-apis/09.03.solution/dn2ncwjsbmo/index.test.ts
2
+ new file mode 100644
3
+ index 0000000..e69de29
4
+ diff --git var/folders/kt/zd3bfncd0c3gjx25hbcq483c0000gn/T/epicshop/diff/advanced-react-apis/playground/dn2ncwjsbmo/index.tsx var/folders/kt/zd3bfncd0c3gjx25hbcq483c0000gn/T/epicshop/diff/advanced-react-apis/09.03.solution/dn2ncwjsbmo/index.tsx
5
+ index 4d68325..fd576f7 100644
6
+ --- var/folders/kt/zd3bfncd0c3gjx25hbcq483c0000gn/T/epicshop/diff/advanced-react-apis/playground/dn2ncwjsbmo/index.tsx
7
+ +++ var/folders/kt/zd3bfncd0c3gjx25hbcq483c0000gn/T/epicshop/diff/advanced-react-apis/09.03.solution/dn2ncwjsbmo/index.tsx
8
+ @@ -1,190 +1,54 @@
9
+ -import { createContext, useEffect, useState, use, useCallback } from 'react'
10
+ +import { Suspense, useSyncExternalStore } from 'react'
11
+ import * as ReactDOM from 'react-dom/client'
12
+ -import {
13
+ - type BlogPost,
14
+ - generateGradient,
15
+ - getMatchingPosts,
16
+ -} from '#shared/blog-posts'
17
+ -import { setGlobalSearchParams } from '#shared/utils'
18
+
19
+ -type SearchParamsTuple = readonly [
20
+ - URLSearchParams,
21
+ - typeof setGlobalSearchParams,
22
+ -]
23
+ -const SearchParamsContext = createContext<SearchParamsTuple>([
24
+ - new URLSearchParams(window.location.search),
25
+ - setGlobalSearchParams,
26
+ -])
27
+ -
28
+ -function SearchParamsProvider({ children }: { children: React.ReactNode }) {
29
+ - const [searchParams, setSearchParamsState] = useState(
30
+ - () => new URLSearchParams(window.location.search),
31
+ - )
32
+ +export function makeMediaQueryStore(mediaQuery: string) {
33
+ + function getSnapshot() {
34
+ + return window.matchMedia(mediaQuery).matches
35
+ + }
36
+
37
+ - useEffect(() => {
38
+ - function updateSearchParams() {
39
+ - setSearchParamsState((prevParams) => {
40
+ - const newParams = new URLSearchParams(window.location.search)
41
+ - return prevParams.toString() === newParams.toString()
42
+ - ? prevParams
43
+ - : newParams
44
+ - })
45
+ + function subscribe(callback: () => void) {
46
+ + const mediaQueryList = window.matchMedia(mediaQuery)
47
+ + mediaQueryList.addEventListener('change', callback)
48
+ + return () => {
49
+ + mediaQueryList.removeEventListener('change', callback)
50
+ }
51
+ - window.addEventListener('popstate', updateSearchParams)
52
+ - return () => window.removeEventListener('popstate', updateSearchParams)
53
+ - }, [])
54
+ -
55
+ - const setSearchParams = useCallback(
56
+ - (...args: Parameters<typeof setGlobalSearchParams>) => {
57
+ - const searchParams = setGlobalSearchParams(...args)
58
+ - setSearchParamsState((prevParams) => {
59
+ - return prevParams.toString() === searchParams.toString()
60
+ - ? prevParams
61
+ - : searchParams
62
+ - })
63
+ - return searchParams
64
+ - },
65
+ - [],
66
+ - )
67
+ -
68
+ - const searchParamsTuple = [searchParams, setSearchParams] as const
69
+ -
70
+ - return (
71
+ - <SearchParamsContext value={searchParamsTuple}>
72
+ - {children}
73
+ - </SearchParamsContext>
74
+ - )
75
+ -}
76
+ -
77
+ -function useSearchParams() {
78
+ - return use(SearchParamsContext)
79
+ -}
80
+ -
81
+ -const getQueryParam = (params: URLSearchParams) => params.get('query') ?? ''
82
+ -
83
+ -function App() {
84
+ - return (
85
+ - <SearchParamsProvider>
86
+ - <div className="app">
87
+ - <Form />
88
+ - <MatchingPosts />
89
+ - </div>
90
+ - </SearchParamsProvider>
91
+ - )
92
+ -}
93
+ -
94
+ -function Form() {
95
+ - const [searchParams, setSearchParams] = useSearchParams()
96
+ - const query = getQueryParam(searchParams)
97
+ -
98
+ - const words = query.split(' ').map((w) => w.trim())
99
+ -
100
+ - const dogChecked = words.includes('dog')
101
+ - const catChecked = words.includes('cat')
102
+ - const caterpillarChecked = words.includes('caterpillar')
103
+ -
104
+ - function handleCheck(tag: string, checked: boolean) {
105
+ - const newWords = checked ? [...words, tag] : words.filter((w) => w !== tag)
106
+ - setSearchParams(
107
+ - { query: newWords.filter(Boolean).join(' ').trim() },
108
+ - { replace: true },
109
+ - )
110
+ }
111
+
112
+ - return (
113
+ - <form onSubmit={(e) => e.preventDefault()}>
114
+ - <div>
115
+ - <label htmlFor="searchInput">Search:</label>
116
+ - <input
117
+ - id="searchInput"
118
+ - name="query"
119
+ - type="search"
120
+ - value={query}
121
+ - onChange={(e) =>
122
+ - setSearchParams({ query: e.currentTarget.value }, { replace: true })
123
+ - }
124
+ - />
125
+ - </div>
126
+ - <div>
127
+ - <label>
128
+ - <input
129
+ - type="checkbox"
130
+ - checked={dogChecked}
131
+ - onChange={(e) => handleCheck('dog', e.currentTarget.checked)}
132
+ - />{' '}
133
+ - 🐶 dog
134
+ - </label>
135
+ - <label>
136
+ - <input
137
+ - type="checkbox"
138
+ - checked={catChecked}
139
+ - onChange={(e) => handleCheck('cat', e.currentTarget.checked)}
140
+ - />{' '}
141
+ - 🐱 cat
142
+ - </label>
143
+ - <label>
144
+ - <input
145
+ - type="checkbox"
146
+ - checked={caterpillarChecked}
147
+ - onChange={(e) =>
148
+ - handleCheck('caterpillar', e.currentTarget.checked)
149
+ - }
150
+ - />{' '}
151
+ - 🐛 caterpillar
152
+ - </label>
153
+ - </div>
154
+ - </form>
155
+ - )
156
+ + return function useMediaQuery() {
157
+ + return useSyncExternalStore(subscribe, getSnapshot)
158
+ + }
159
+ }
160
+
161
+ -function MatchingPosts() {
162
+ - const [searchParams] = useSearchParams()
163
+ - const query = getQueryParam(searchParams)
164
+ - const matchingPosts = getMatchingPosts(query)
165
+ +const useNarrowMediaQuery = makeMediaQueryStore('(max-width: 600px)')
166
+
167
+ - return (
168
+ - <ul className="post-list">
169
+ - {matchingPosts.map((post) => (
170
+ - <Card key={post.id} post={post} />
171
+ - ))}
172
+ - </ul>
173
+ - )
174
+ +function NarrowScreenNotifier() {
175
+ + const isNarrow = useNarrowMediaQuery()
176
+ + return isNarrow ? 'You are on a narrow screen' : 'You are on a wide screen'
177
+ }
178
+
179
+ -function Card({ post }: { post: BlogPost }) {
180
+ - const [isFavorited, setIsFavorited] = useState(false)
181
+ +function App() {
182
+ return (
183
+ - <li>
184
+ - {isFavorited ? (
185
+ - <button
186
+ - aria-label="Remove favorite"
187
+ - onClick={() => setIsFavorited(false)}
188
+ - >
189
+ - ❤️
190
+ - </button>
191
+ - ) : (
192
+ - <button aria-label="Add favorite" onClick={() => setIsFavorited(true)}>
193
+ - 🤍
194
+ - </button>
195
+ - )}
196
+ - <div
197
+ - className="post-image"
198
+ - style={{ background: generateGradient(post.id) }}
199
+ - />
200
+ - <a
201
+ - href={post.id}
202
+ - onClick={(event) => {
203
+ - event.preventDefault()
204
+ - alert(`Great! Let's go to ${post.id}!`)
205
+ - }}
206
+ - >
207
+ - <h2>{post.title}</h2>
208
+ - <p>{post.description}</p>
209
+ - </a>
210
+ - </li>
211
+ + <div>
212
+ + <div>This is your narrow screen state:</div>
213
+ + <Suspense fallback="">
214
+ + <NarrowScreenNotifier />
215
+ + </Suspense>
216
+ + </div>
217
+ )
218
+ }
219
+
220
+ const rootEl = document.createElement('div')
221
+ document.body.append(rootEl)
222
+ -ReactDOM.createRoot(rootEl).render(<App />)
223
+ +// 🦉 here's how we pretend we're server-rendering
224
+ +rootEl.innerHTML = (await import('react-dom/server')).renderToString(<App />)
225
+ +
226
+ +// 🦉 here's how we simulate a delay in hydrating with client-side js
227
+ +await new Promise((resolve) => setTimeout(resolve, 1000))
228
+ +
229
+ +ReactDOM.hydrateRoot(rootEl, <App />, {
230
+ + onRecoverableError(error) {
231
+ + if (String(error).includes('Missing getServerSnapshot')) return
232
+ +
233
+ + console.error(error)
234
+ + },
235
+ +})
0 commit comments