-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
0165794
commit 477449c
Showing
10 changed files
with
200 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,33 @@ | ||
# useSyncExternalStore | ||
|
||
🦉 When you have a design that needs to be responsive, you use | ||
[media queries](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries) | ||
to change the layout of the page based on the size of the screen. Media queries | ||
can tell you a lot more than just the width of the page and sometimes you need | ||
to know whether a media query matches even outside of a CSS context. | ||
|
||
The browser supports a JavaScript API called `matchMedia` that allows you to | ||
query the current state of a media query: | ||
|
||
```tsx | ||
const prefersDarkModeQuery = window.matchMedia('(prefers-color-scheme: dark)') | ||
console.log(prefersDarkModeQuery.matches) // true if the user prefers dark mode | ||
``` | ||
|
||
👨💼 Thanks for that Olivia. So yes, our users want a component that displays | ||
whether they're on a narrow screen. We're going to build this into a more | ||
generic hook that will allow us to determine any media query's match and also | ||
keep the state in sync with the media query. And you're going to need to use | ||
`useSyncExternalStore` to do it. | ||
|
||
Go ahead and follow the emoji instructions. You'll know you got it right when | ||
you resize your screen and the text changes. | ||
|
||
<callout-info class="aside"> | ||
🦉 If we really were just trying to display some different text based on the | ||
screen size, we could use CSS media queries and not have to write any | ||
JavaScript at all. But sometimes we need to know the state of a media query in | ||
JavaScript for more complex interactions, so we're going to use a simple | ||
example to demonstrate how to do this to handle those cases and we'll be using | ||
`useSyncExternalStore` for that. | ||
</callout-info> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,3 @@ | ||
# useSyncExternalStore | ||
|
||
👨💼 Great work! Our users will now know whether they're on a narrow screen 🤡 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,6 @@ | ||
# Make Store Utility | ||
|
||
👨💼 We want to make this utility generally useful so we can use it for any media | ||
query. So please stick most of our logic in a `makeMediaQueryStore` function | ||
and have that return a custom hook people can use to keep track of the current | ||
media query's matching state. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,4 @@ | ||
# Make Store Utility | ||
|
||
👨💼 Great! With that we now have a reusable utility and can use this to subscribe | ||
to any media query! |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,27 @@ | ||
# Handling Server Rendering | ||
|
||
👨💼 We don't currently do any server rendering, but in the future we may want to | ||
and this requires some special handling with `useSyncExternalStore`. | ||
|
||
🧝♂️ I've simulated a server rendering environment by adding some code to the | ||
bottom of our file. First, we render the `<App />` to a string, then we set that | ||
to the `innerHTML` of our `rootEl`. Then we call `hydrateRoot` to rehydrate our | ||
application. | ||
|
||
👨💼 This is a bit of a hack, but it's a good way to simulate server rendering | ||
and ensure that our application works in a server rendering situation. | ||
|
||
Because the server won't know whether a media query matches, we can't use the | ||
`getServerSnapshot()` argument of `useSyncExternalStore`. Instead, we'll leave | ||
that argument off, and wrap our `<NarrowScreenNotifier />` in a `<Suspense />` | ||
component with a fallback of `""` (we won't show anything until the client | ||
hydrates). | ||
|
||
With this, you'll notice there's an error in the console. Nothing's technically | ||
wrong, but React logs this in this situation (I honestly personally disagree | ||
that they should do this, but 🤷♂️). So as extra credit, you can add an | ||
`onRecoverableError` function to the `hydrateRoot` call and if the given error | ||
includes the string `'Missing getServerSnapshot'` then you can return, | ||
otherwise, log the error. | ||
|
||
Good luck! |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,10 @@ | ||
# Handling Server Rendering | ||
|
||
👨💼 Great work! You now know how to properly handle server rendering of something | ||
we don't know until the client-render when it comes to an external store like | ||
this. | ||
|
||
🦉 There are more things you can do for different cases (like the user's | ||
light/dark mode preference) to offer a better user experience. Check out | ||
[`@epic-web/client-hints`](https://www.npmjs.com/package/@epic-web/client-hints) | ||
to see how you can handle this even better if you're interested. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,4 @@ | ||
# Sync External State | ||
|
||
👨💼 Great work! You now know how to integrate React with external bits of | ||
changing state. Well done! |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,101 @@ | ||
# Sync External State | ||
|
||
Not everything we build on the web is built with React. There are libraries and | ||
platform APIs that are external to React. Bringing those things into React's | ||
component model and state management lifecycle is necessary for building | ||
full-featured applications. | ||
|
||
The task is to synchronize the external world with the internal state of a | ||
React component. To do this, we use the `useSyncExternalStore` hook. | ||
|
||
Let's take the example of a component that displays your current location via | ||
the geolocation API. The geolocation API is not a part of React, so we need to | ||
synchronize the external state of the geolocation API with the internal state of | ||
our component. | ||
|
||
```tsx lines=31-35 | ||
import { useSyncExternalStore } from 'react' | ||
|
||
type LocationData = | ||
| { status: 'unavailable'; geo?: never } | ||
| { status: 'available'; geo: GeolocationPosition } | ||
// this variable is our external store! | ||
let location: LocationData = { status: 'unavailable' } | ||
|
||
function subscribeToGeolocation(callback: () => void) { | ||
const watchId = navigator.geolocation.watchPosition(position => { | ||
location = { status: 'available', geo: position } | ||
callback() | ||
}) | ||
return () => { | ||
location = { status: 'unavailable' } | ||
return navigator.geolocation.clearWatch(watchId) | ||
} | ||
} | ||
|
||
function getGeolocationSnapshot() { | ||
return location | ||
} | ||
|
||
function MyLocation() { | ||
const location = useSyncExternalStore( | ||
subscribeToGeolocation, | ||
getGeolocationSnapshot, | ||
) | ||
return ( | ||
<div> | ||
{location.status === 'unavailable' ? ( | ||
'Your location is unavailable' | ||
) : ( | ||
<> | ||
Your location is {location.geo.coords.latitude.toFixed(2)} | ||
{'°, '} | ||
{location.geo.coords.longitude.toFixed(2)} | ||
{'°'} | ||
</> | ||
)} | ||
</div> | ||
) | ||
} | ||
|
||
function App() { | ||
return ( | ||
<Suspense fallback="loading location..."> | ||
<MyLocation /> | ||
</Suspense> | ||
) | ||
} | ||
``` | ||
|
||
Here's the basic API: | ||
|
||
```tsx | ||
const snapshot = useSyncExternalStore( | ||
subscribe, | ||
getSnapshot, | ||
getServerSnapshot, // optional | ||
) | ||
``` | ||
|
||
- `subscribe` is a function that takes a callback and returns a cleanup function. | ||
The callback is called whenever the external store changes to let React know | ||
it should call `getSnapshot` to get the new value. | ||
- `getSnapshot` is a function that returns the current value of the external | ||
store. | ||
- `getServerSnapshot` is an optional function that returns the current value of | ||
the external store from the server. This is useful for server-side rendering | ||
and rehydration. If you don't provide this function, then React will render | ||
the nearest `Suspense` boundary `fallback` on the server and then when the | ||
client hydrates, it will call `getSnapshot` to get the current value. | ||
|
||
<callout-success class="aside"> | ||
To learn more about server rendering React, check [the | ||
docs](https://react.dev/reference/react-dom/server). | ||
</callout-success> | ||
|
||
The `Suspense` Component is something we'll get to in a future workshop. For | ||
now, think of it as a way to declaratively handle loading states in React | ||
(because that's what it is and that's all you need to know for now). | ||
|
||
Learn more about `useSyncExternalStore` in the | ||
[API documentation](https://react.dev/reference/react/useSyncExternalStore). |