diff --git a/compiler/packages/babel-plugin-react-compiler/src/index.ts b/compiler/packages/babel-plugin-react-compiler/src/index.ts index d2abd744d6b48..ca5b65343014f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/index.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/index.ts @@ -29,10 +29,13 @@ export { ProgramContext, tryFindDirectiveEnablingMemoization as findDirectiveEnablingMemoization, findDirectiveDisablingMemoization, + defaultOptions, type CompilerPipelineValue, type Logger, type LoggerEvent, type PluginOptions, + type AutoDepsDecorationsEvent, + type CompileSuccessEvent, } from './Entrypoint'; export { Effect, diff --git a/compiler/packages/react-forgive/package.json b/compiler/packages/react-forgive/package.json index 0bf48e232ecf4..fc7d71095c51c 100644 --- a/compiler/packages/react-forgive/package.json +++ b/compiler/packages/react-forgive/package.json @@ -36,13 +36,14 @@ }, "scripts": { "build": "yarn run compile", + "build:compiler": "yarn workspace babel-plugin-react-compiler build --dts", "compile": "rimraf dist && concurrently -n server,client \"scripts/build.mjs -t server\" \"scripts/build.mjs -t client\"", "dev": "yarn run package && yarn run install-ext", "install-ext": "code --install-extension react-forgive-0.0.0.vsix", "lint": "echo 'no tests'", "package": "rm -f react-forgive-0.0.0.vsix && vsce package --yarn", "postinstall": "cd client && yarn install && cd ../server && yarn install && cd ..", - "pretest": "yarn run compile && yarn run lint", + "pretest": "yarn run build:compiler && yarn run compile && yarn run lint", "test": "vscode-test", "vscode:prepublish": "yarn run compile", "watch": "scripts/build.mjs --watch" diff --git a/compiler/packages/react-forgive/server/package.json b/compiler/packages/react-forgive/server/package.json index fb6f4feebd7ac..c91258607205f 100644 --- a/compiler/packages/react-forgive/server/package.json +++ b/compiler/packages/react-forgive/server/package.json @@ -18,7 +18,6 @@ "@babel/parser": "^7.26.0", "@babel/plugin-syntax-typescript": "^7.25.9", "@babel/types": "^7.26.0", - "cosmiconfig": "^9.0.0", "prettier": "^3.3.3", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.12" diff --git a/compiler/packages/react-forgive/server/src/compiler/compat.ts b/compiler/packages/react-forgive/server/src/compiler/compat.ts index 10271cbdcdc32..f467f70ba1361 100644 --- a/compiler/packages/react-forgive/server/src/compiler/compat.ts +++ b/compiler/packages/react-forgive/server/src/compiler/compat.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import {SourceLocation} from 'babel-plugin-react-compiler/src'; +import {type SourceLocation} from 'babel-plugin-react-compiler'; import {type Range} from 'vscode-languageserver'; export function babelLocationToRange(loc: SourceLocation): Range | null { diff --git a/compiler/packages/react-forgive/server/src/compiler/index.ts b/compiler/packages/react-forgive/server/src/compiler/index.ts index fe192c62132b6..b474253f71e11 100644 --- a/compiler/packages/react-forgive/server/src/compiler/index.ts +++ b/compiler/packages/react-forgive/server/src/compiler/index.ts @@ -9,7 +9,7 @@ import type * as BabelCore from '@babel/core'; import {parseAsync, transformFromAstAsync} from '@babel/core'; import BabelPluginReactCompiler, { type PluginOptions, -} from 'babel-plugin-react-compiler/src'; +} from 'babel-plugin-react-compiler'; import * as babelParser from 'prettier/plugins/babel.js'; import estreeParser from 'prettier/plugins/estree'; import * as typescriptParser from 'prettier/plugins/typescript'; diff --git a/compiler/packages/react-forgive/server/src/compiler/options.ts b/compiler/packages/react-forgive/server/src/compiler/options.ts deleted file mode 100644 index 226be799d3abc..0000000000000 --- a/compiler/packages/react-forgive/server/src/compiler/options.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import { - parsePluginOptions, - type PluginOptions, -} from 'babel-plugin-react-compiler/src'; -import {cosmiconfigSync} from 'cosmiconfig'; - -export function resolveReactConfig(projectPath: string): PluginOptions | null { - const explorerSync = cosmiconfigSync('react', { - searchStrategy: 'project', - cache: true, - }); - const result = explorerSync.search(projectPath); - if (result != null) { - return parsePluginOptions(result.config); - } else { - return null; - } -} diff --git a/compiler/packages/react-forgive/server/src/index.ts b/compiler/packages/react-forgive/server/src/index.ts index fd4ffd99886c9..7552337cbc7a8 100644 --- a/compiler/packages/react-forgive/server/src/index.ts +++ b/compiler/packages/react-forgive/server/src/index.ts @@ -20,13 +20,12 @@ import { TextDocumentSyncKind, } from 'vscode-languageserver/node'; import {compile, lastResult} from './compiler'; -import {type PluginOptions} from 'babel-plugin-react-compiler/src'; -import {resolveReactConfig} from './compiler/options'; import { type CompileSuccessEvent, type LoggerEvent, + type PluginOptions, defaultOptions, -} from 'babel-plugin-react-compiler/src/Entrypoint/Options'; +} from 'babel-plugin-react-compiler'; import {babelLocationToRange, getRangeFirstCharacter} from './compiler/compat'; import { type AutoDepsDecorationsLSPEvent, @@ -64,8 +63,7 @@ type CodeActionLSPEvent = { }; connection.onInitialize((_params: InitializeParams) => { - // TODO(@poteto) get config fr - compilerOptions = resolveReactConfig('.') ?? defaultOptions; + compilerOptions = defaultOptions; compilerOptions = { ...compilerOptions, environment: { @@ -76,21 +74,21 @@ connection.onInitialize((_params: InitializeParams) => { importSpecifierName: 'useEffect', source: 'react', }, - numRequiredArgs: 1, + autodepsIndex: 1, }, { function: { importSpecifierName: 'useSpecialEffect', source: 'shared-runtime', }, - numRequiredArgs: 2, + autodepsIndex: 2, }, { function: { importSpecifierName: 'default', source: 'useEffectWrapper', }, - numRequiredArgs: 1, + autodepsIndex: 1, }, ], }, diff --git a/compiler/packages/react-forgive/server/src/requests/autodepsdecorations.ts b/compiler/packages/react-forgive/server/src/requests/autodepsdecorations.ts index 77a568662e48c..c9c9e8ca01331 100644 --- a/compiler/packages/react-forgive/server/src/requests/autodepsdecorations.ts +++ b/compiler/packages/react-forgive/server/src/requests/autodepsdecorations.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import {type AutoDepsDecorationsEvent} from 'babel-plugin-react-compiler/src/Entrypoint'; +import {type AutoDepsDecorationsEvent} from 'babel-plugin-react-compiler'; import {type Position} from 'vscode-languageserver-textdocument'; import {RequestType} from 'vscode-languageserver/node'; import {type Range, sourceLocationToRange} from '../utils/range'; diff --git a/compiler/packages/react-forgive/server/yarn.lock b/compiler/packages/react-forgive/server/yarn.lock index b72063294ff08..ee346ce7d3a54 100644 --- a/compiler/packages/react-forgive/server/yarn.lock +++ b/compiler/packages/react-forgive/server/yarn.lock @@ -10,7 +10,7 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.24" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.0", "@babel/code-frame@^7.26.2": +"@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.0", "@babel/code-frame@^7.26.2": version "7.26.2" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== @@ -188,11 +188,6 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -argparse@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" - integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== - browserslist@^4.24.0: version "4.24.3" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.3.tgz#5fc2725ca8fb3c1432e13dac278c7cc103e026d2" @@ -203,11 +198,6 @@ browserslist@^4.24.0: node-releases "^2.0.19" update-browserslist-db "^1.1.1" -callsites@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" - integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== - caniuse-lite@^1.0.30001688: version "1.0.30001690" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz#f2d15e3aaf8e18f76b2b8c1481abde063b8104c8" @@ -218,16 +208,6 @@ convert-source-map@^2.0.0: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== -cosmiconfig@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-9.0.0.tgz#34c3fc58287b915f3ae905ab6dc3de258b55ad9d" - integrity sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg== - dependencies: - env-paths "^2.2.1" - import-fresh "^3.3.0" - js-yaml "^4.1.0" - parse-json "^5.2.0" - debug@^4.1.0, debug@^4.3.1: version "4.4.0" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" @@ -240,18 +220,6 @@ electron-to-chromium@^1.5.73: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.74.tgz#cb886b504a6467e4c00bea3317edb38393c53413" integrity sha512-ck3//9RC+6oss/1Bh9tiAVFy5vfSKbRHAFh7Z3/eTRkEqJeWgymloShB17Vg3Z4nmDNp35vAd1BZ6CMW4Wt6Iw== -env-paths@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" - integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== - -error-ex@^1.3.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - escalade@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" @@ -267,51 +235,21 @@ globals@^11.1.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -import-fresh@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" - integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== - dependencies: - parent-module "^1.0.0" - resolve-from "^4.0.0" - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== - js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== - dependencies: - argparse "^2.0.1" - jsesc@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== -json-parse-even-better-errors@^2.3.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" - integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== - json5@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== -lines-and-columns@^1.1.6: - version "1.2.4" - resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" - integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== - lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -329,23 +267,6 @@ node-releases@^2.0.19: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== -parent-module@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" - integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== - dependencies: - callsites "^3.0.0" - -parse-json@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" - integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== - dependencies: - "@babel/code-frame" "^7.0.0" - error-ex "^1.3.1" - json-parse-even-better-errors "^2.3.0" - lines-and-columns "^1.1.6" - picocolors@^1.0.0, picocolors@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" @@ -356,11 +277,6 @@ prettier@^3.3.3: resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.4.2.tgz#a5ce1fb522a588bf2b78ca44c6e6fe5aa5a2b13f" integrity sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ== -resolve-from@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" - integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== - semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index fe8722da568da..9de9037690cb5 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -1913,6 +1913,20 @@ export function attach( return false; } + function isUseSyncExternalStoreHook(hookObject: any): boolean { + const queue = hookObject.queue; + if (!queue) { + return false; + } + + const boundHasOwnProperty = hasOwnProperty.bind(queue); + return ( + boundHasOwnProperty('value') && + boundHasOwnProperty('getSnapshot') && + typeof queue.getSnapshot === 'function' + ); + } + function isHookThatCanScheduleUpdate(hookObject: any) { const queue = hookObject.queue; if (!queue) { @@ -1929,12 +1943,7 @@ export function attach( return true; } - // Detect useSyncExternalStore() - return ( - boundHasOwnProperty('value') && - boundHasOwnProperty('getSnapshot') && - typeof queue.getSnapshot === 'function' - ); + return isUseSyncExternalStoreHook(hookObject); } function didStatefulHookChange(prev: any, next: any): boolean { @@ -1955,10 +1964,18 @@ export function attach( const indices = []; let index = 0; + while (next !== null) { if (didStatefulHookChange(prev, next)) { indices.push(index); } + + // useSyncExternalStore creates 2 internal hooks, but we only count it as 1 user-facing hook + if (isUseSyncExternalStoreHook(next)) { + next = next.next; + prev = prev.next; + } + next = next.next; prev = prev.next; index++; diff --git a/packages/react-devtools-shell/src/app/InspectableElements/InspectableElements.js b/packages/react-devtools-shell/src/app/InspectableElements/InspectableElements.js index 95f104925b93d..34f46774b188b 100644 --- a/packages/react-devtools-shell/src/app/InspectableElements/InspectableElements.js +++ b/packages/react-devtools-shell/src/app/InspectableElements/InspectableElements.js @@ -20,6 +20,7 @@ import SimpleValues from './SimpleValues'; import SymbolKeys from './SymbolKeys'; import UseMemoCache from './UseMemoCache'; import UseEffectEvent from './UseEffectEvent'; +import UseSyncExternalStore from './UseSyncExternalStore'; // TODO Add Immutable JS example @@ -38,6 +39,7 @@ export default function InspectableElements(): React.Node { + ); } diff --git a/packages/react-devtools-shell/src/app/InspectableElements/UseSyncExternalStore.js b/packages/react-devtools-shell/src/app/InspectableElements/UseSyncExternalStore.js new file mode 100644 index 0000000000000..decb10b2db2d5 --- /dev/null +++ b/packages/react-devtools-shell/src/app/InspectableElements/UseSyncExternalStore.js @@ -0,0 +1,133 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import * as React from 'react'; + +const {useState, useEffect, useSyncExternalStore} = React; + +// Create a simple external store for demonstratio +function createStore(initialValue: T): { + subscribe: (cb: () => void) => () => any, + getSnapshot: () => T, + setValue: (newValue: T) => void, +} { + let value = initialValue; + const subscribers = new Set<() => void>(); + + return { + subscribe(callback) { + subscribers.add(callback); + return () => subscribers.delete(callback); + }, + getSnapshot() { + return value; + }, + setValue(newValue) { + value = newValue; + subscribers.forEach(callback => callback()); + }, + }; +} + +const counterStore = createStore(0); +const themeStore = createStore('light'); + +export default function UseSyncExternalStore(): React.Node { + return ( + <> +

useSyncExternalStore()

+ + + + + ); +} + +function SingleHookCase(): React.Node { + const count = useSyncExternalStore( + counterStore.subscribe, + counterStore.getSnapshot, + ); + + return ( +
+

Single hook case

+

Count: {count}

+ + +
+ ); +} + +function useCounter() { + const count = useSyncExternalStore( + counterStore.subscribe, + counterStore.getSnapshot, + ); + const [localState, setLocalState] = useState(0); + + useEffect(() => { + // Some effect + }, [count]); + + return {count, localState, setLocalState}; +} + +function HookTreeCase(): React.Node { + const {count, localState, setLocalState} = useCounter(); + + return ( +
+

Hook tree case

+

External count: {count}

+

Local state: {localState}

+ + +
+ ); +} + +function useTheme() { + const theme = useSyncExternalStore( + themeStore.subscribe, + themeStore.getSnapshot, + ); + + return theme; +} + +function MultipleStoresCase() { + const count = useSyncExternalStore( + counterStore.subscribe, + counterStore.getSnapshot, + ); + const theme = useTheme(); + + return ( +
+

Multiple stores case

+

Count: {count}

+

Theme: {theme}

+ +
+ ); +}