June.

11월 안에는 꼭...

Portfolio

About

React 18 about useSyncExternalStore

2023/10/15
짧.

3 min read

React 18 about useSyncExternalStore

Locales:

en

ko

tl;dr

There are several libraries out there that help you manage state in React without using React's internal hooks, such as useState and useReducer. hooks inside React, such as useState and useReducer. These include redux, recoil, jotai, xtsate, zustand, rect query... to name a few.

Simply put, useSyncExternalStore allows you to synchronize a store that doesn't rely on React (the libraries listed above) and track changes via useState and useEffect, and synchronize them with React's re-rendering through the setter of useState.

useSyncExternalStore

The official documentation is located here. Intuitively, if you break it down, it's use + sync external store, a hook to sync with an external store (use).

1const snapshot = useSyncExternalStore( 2 subscribe, 3 getSnapshot, 4 getServerSnapshot 5);

So if you have a stateful library that doesn't have a React dependency, or an external store that doesn't have a React dependency and your rendering logic might be It's a hook that allows you to connect to an external store that might have some issues and get on top of React's re-rendering system.

So how is it implemented internally? The implementation code is located here.

1import * as React from 'react'; 2import is from 'shared/objectIs'; 3 4// Intentionally not using named imports because Rollup uses dynamic 5// dispatch for CommonJS interop named imports. 6const {useState, useEffect, useLayoutEffect, useDebugValue} = React; 7 8let didWarnOld18Alpha = false; 9let didWarnUncachedGetSnapshot = false; 10 11// Disclaimer: This shim breaks many of the rules of React, and only works 12// because of a very particular set of implementation details and assumptions 13// -- change any one of them and it will break. The most important assumption 14// is that updates are always synchronous, because concurrent rendering is 15// only available in versions of React that also have a built-in 16// useSyncExternalStore API. And we only use this shim when the built-in API 17// does not exist. 18// 19// Do not assume that the clever hacks used by this hook also work in general. 20// The point of this shim is to replace the need for hacks by other libraries. 21export function useSyncExternalStore<T>( 22 subscribe: (() => void) => () => void, 23 getSnapshot: () => T, 24 // Note: The shim does not use getServerSnapshot, because pre-18 versions of 25 // React do not expose a way to check if we're hydrating. So users of the shim 26 // will need to track that themselves and return the correct value 27 // from `getSnapshot`. 28 getServerSnapshot?: () => T, 29): T { 30 if (__DEV__) { 31 if (!didWarnOld18Alpha) { 32 if (React.startTransition !== undefined) { 33 didWarnOld18Alpha = true; 34 console.error( 35 'You are using an outdated, pre-release alpha of React 18 that ' + 36 'does not support useSyncExternalStore. The ' + 37 'use-sync-external-store shim will not work correctly. Upgrade ' + 38 'to a newer pre-release.', 39 ); 40 } 41 } 42 } 43 44 // Read the current snapshot from the store on every render. Again, this 45 // breaks the rules of React, and only works here because of specific 46 // implementation details, most importantly that updates are 47 // always synchronous. 48 const value = getSnapshot(); 49 if (__DEV__) { 50 if (!didWarnUncachedGetSnapshot) { 51 const cachedValue = getSnapshot(); 52 if (!is(value, cachedValue)) { 53 console.error( 54 'The result of getSnapshot should be cached to avoid an infinite loop', 55 ); 56 didWarnUncachedGetSnapshot = true; 57 } 58 } 59 } 60 61 // Because updates are synchronous, we don't queue them. Instead we force a 62 // re-render whenever the subscribed state changes by updating an some 63 // arbitrary useState hook. Then, during render, we call getSnapshot to read 64 // the current value. 65 // 66 // Because we don't actually use the state returned by the useState hook, we 67 // can save a bit of memory by storing other stuff in that slot. 68 // 69 // To implement the early bailout, we need to track some things on a mutable 70 // object. Usually, we would put that in a useRef hook, but we can stash it in 71 // our useState hook instead. 72 // 73 // To force a re-render, we call forceUpdate({inst}). That works because the 74 // new object always fails an equality check. 75 const [{inst}, forceUpdate] = useState({inst: {value, getSnapshot}}); 76 77 // Track the latest getSnapshot function with a ref. This needs to be updated 78 // in the layout phase so we can access it during the tearing check that 79 // happens on subscribe. 80 useLayoutEffect(() => { 81 inst.value = value; 82 inst.getSnapshot = getSnapshot; 83 84 // Whenever getSnapshot or subscribe changes, we need to check in the 85 // commit phase if there was an interleaved mutation. In concurrent mode 86 // this can happen all the time, but even in synchronous mode, an earlier 87 // effect may have mutated the store. 88 if (checkIfSnapshotChanged(inst)) { 89 // Force a re-render. 90 forceUpdate({inst}); 91 } 92 }, [subscribe, value, getSnapshot]); 93 94 useEffect(() => { 95 // Check for changes right before subscribing. Subsequent changes will be 96 // detected in the subscription handler. 97 if (checkIfSnapshotChanged(inst)) { 98 // Force a re-render. 99 forceUpdate({inst}); 100 } 101 const handleStoreChange = () => { 102 // TODO: Because there is no cross-renderer API for batching updates, it's 103 // up to the consumer of this library to wrap their subscription event 104 // with unstable_batchedUpdates. Should we try to detect when this isn't 105 // the case and print a warning in development? 106 107 // The store changed. Check if the snapshot changed since the last time we 108 // read from the store. 109 if (checkIfSnapshotChanged(inst)) { 110 // Force a re-render. 111 forceUpdate({inst}); 112 } 113 }; 114 // Subscribe to the store and return a clean-up function. 115 return subscribe(handleStoreChange); 116 }, [subscribe]); 117 118 useDebugValue(value); 119 return value; 120} 121 122function checkIfSnapshotChanged<T>(inst: { 123 value: T, 124 getSnapshot: () => T, 125}): boolean { 126 const latestGetSnapshot = inst.getSnapshot; 127 const prevValue = inst.value; 128 try { 129 const nextValue = latestGetSnapshot(); 130 return !is(prevValue, nextValue); 131 } catch (error) { 132 return true; 133 } 134}

The code looks a bit long, but if you remove the code from the DEV environment and remove all comments, the code is not that long.

1export function useSyncExternalStore<T>( 2 subscribe: (() => void) => () => void, 3 getSnapshot: () => T, 4 getServerSnapshot?: () => T, 5): T { 6 const value = getSnapshot(); 7 const [{inst}, forceUpdate] = useState({inst: {value, getSnapshot}}); 8 9 useLayoutEffect(() => { 10 inst.value = value; 11 inst.getSnapshot = getSnapshot; 12 13 if (checkIfSnapshotChanged(inst)) { 14 forceUpdate({inst}); 15 } 16 }, [subscribe, value, getSnapshot]); 17 18 useEffect(() => { 19 if (checkIfSnapshotChanged(inst)) { 20 forceUpdate({inst}); 21 } 22 const handleStoreChange = () => { 23 if (checkIfSnapshotChanged(inst)) { 24 forceUpdate({inst}); 25 } 26 }; 27 return subscribe(handleStoreChange); 28 }, [subscribe]); 29 30 useDebugValue(value); 31 return value; 32}

What do you think, it's not that long, right?

The main logic is to get the snapshot value via the getSnapshot function from the user, If that snapshot has changed, the only logic is to force an update and re-render.

The checkIfSnapshotChanged function is implemented as follows.

1function checkIfSnapshotChanged<T>(inst: { 2 value: T; 3 getSnapshot: () => T; 4}): boolean { 5 const latestGetSnapshot = inst.getSnapshot; 6 const prevValue = inst.value; 7 try { 8 const nextValue = latestGetSnapshot(); 9 return !is(prevValue, nextValue); 10 } catch (error) { 11 return true; 12 } 13}

This function checks if the snapshot has changed in the useSyncExternalStore hook, It is used to force a re-render when the snapshot has changed. If a change is detected, the re-render is forced via forceUpdate.

So useSyncExternalStore is used for external stores implemented without using React's internal hooks. external store implemented without using React's internal hooks to get on React's rendering system.

관련 포스트가 4개 있어요.

useSyncExternalStore가 무엇일까?

짧.
react-use-sync-external-store cover image
2023/10/23 (updated)

React 18 useSyncExternalStore에 대해서

CSS flex box의 align-items의 flex-start와 baseline의 차이점에 대해서

짧.
css-flex-box-align-items-attributes cover image
2023/09/24

CSS에서 align-items의 flex-start와 baseline의 차이점

React 18.0.0 react-dom의 flushSync의 사용법과 주의사항에 대해서 알아보자.

짧.
react-flush-sync cover image
2023/09/24

React flushSync에 대해서

maxLength를 넣고 이모지를 넣으면 브라우저마다 계산되는 것이 다르다.

짧.
html-input-max-length cover image
2023/09/15

HTML `<input />`의 maxLength 동작이 safari에서 다른 이슈

profile

정현수.

Currently Managed

Currently not managed

© 2024. junghyeonsu all rights reserved.