Repository navigation
fix(native): make colorScheme.set move every reader of the scheme - #415
YevheniiKotyrlo wants to merge 1 commit into
Conversation
|
Outdated: this compared the August heads of #415 and #429. #415 is now re-written onto The original noteCross-referencing #415 and #429 — they merge with zero conflicts and the result does not compile. Flagging it now, because nothing will warn whoever lands the second one.
The two halves are each correct alone:
On the merge, #429's wider union reaches a call react-native 0.81 types as Neither PR can pre-empt this, which is why it is a note rather than a fix on one of them. #415 cannot import The resolution is one line at merge time: route the native setter through the same seam the web one already uses, rather than calling // src/native/api.tsx
import { setAppearanceColorScheme } from "../color-scheme";
…
setAppearanceColorScheme(value); // instead of Appearance.setColorScheme(value)Measured: that takes the merged tree from one error to zero. The seam exists precisely because the one member the supported react-native range declares incompatibly is the "follow the system" write, so this is what it was built for. Worth pairing with a compile-level guard, since the failure is a type error rather than a behaviour: the merged tree must typecheck. A runtime twin already exists on #429 at |
|
Outdated: this compared the August heads of #415 and #429. #415 is now re-written onto The original noteFollowing up on the cross-reference above with two measured additions, one of which is a trap worth naming explicitly. An inline cast at the call site compiles, and is the wrong resolutionThe obvious shortcut for the merge break is to widen the argument in place rather than route through the seam: Appearance.setColorScheme(value as Parameters<typeof Appearance.setColorScheme>[0]);That does take the merged tree to zero type errors, on both react-native ends of the peer range. It is still wrong, because it silences both arms of a union that disagrees for a reason: Up to 0.81.5 a nullish request is coerced to That is the failure #415's own comment forbids in the same function: "Announcing it would put a value in Appearance's cache that no reader can render." The boundary is 0.82.0, not 0.86Measured across every react-native in the Yarn cache, by extracting The type narrowing and the loss of the One nuance worth recording for whoever touches this next, because it is easy to collapse into the same number: there is a third state later in the range, where A latent one, for #415#415's three new colour-scheme test files do not type-check against ≥0.82 — |
colorScheme.set moved this library's observable and nothing else, so the class layer and React Native's own readers disagreed: useColorScheme() and every store built on Appearance.addChangeListener kept the old scheme. The setter now writes through to Appearance and announces the scheme on the appearanceChanged device event, because Appearance.setColorScheme writes its cache without notifying a listener, and that event is the one Appearance itself handles. It announces only a resolved scheme it changes. A follow-the-system request is handed to Appearance as "unspecified"; on react-native 0.82.0-0.84.1, which caches that request verbatim, the scheme in force before the hand-back is put back in the cache. colorScheme.get() and prefers-color-scheme resolve the held value through one function, which answers a scheme for any value that is not one: the reported scheme, then light (MQ5 §5.4). The class layer read the raw observable, so a held null or request matched neither scheme.
eac5a41 to
79fc5d5
Compare


Problem
On
main,colorScheme.set()moves this library's observable and nothing else:useColorScheme()and every store built onAppearance.addChangeListenerkeep the old scheme. An app calling the setter therefore renders itsdark:classes on one scheme and its prop-valued colours on the other.nullafter a follow-the-system request.prefers-color-scheme: lightanddarkthen both fail whilecolorScheme.get()reports a scheme, and the element falls through to its unconditional rule.Solution
set()writes through toAppearance.setColorSchemeand announces the scheme on theappearanceChangeddevice event.Appearance.setColorSchemewrites its cache without notifying a listener.appearanceChangedis the eventAppearanceitself handles, so it performs its own cache write andchangeemit, as it does for an OS change.Appearanceas"unspecified". React Native 0.82.0 to 0.84.1 caches that request verbatim, and the platform echoes nothing because the resolved scheme did not change. So on that band the scheme in force before the hand-back is put back in the cache.colorScheme.get()andprefers-color-schemeresolve the held value throughresolveColorScheme. For any value that is not a scheme, it answers the schemeAppearancereports, thenlight(MQ5 §12.5.1).Tests
color-scheme-appearance.test.tsxruns the installed React Native 0.86Appearanceagainst a fake native module that behaves like both platforms: it applies a write later and reports the scheme in force. It covers:nulland through"unspecified"color-scheme-appearance-cache-rules.test.tsxruns the hand-back scenarios against the three cache-write rules across thereact-native >= 0.81peer range, including a hand-back made throughAppearanceitself.main, 13 of the 31 cases fail. Each of 9 mutations fails its own cases.Verification
On Windows with Node 26:
yarn lintcleanyarn typecheckcleanyarn test --coverage: every failure also fails onmainon this machine (the four babel cases)yarn buildcleanyarn example expo export --platform webexportedDevice evidence, before and after: #415 (comment)
Known limits
colorScheme.setcallsAppearance.setColorScheme, which react-native-web 0.21 does not have, so it throws onmain; what overriding the scheme should mean on web is a separate question.Merge order
prefers-color-schemelookup into one feature lookup. Whichever lands second resolves the scheme there.prefers-reduced-motionbeside theprefers-color-schemecase. Whichever lands second keeps both.colorSchemeinto reactivity's pinned process-global state. Whichever lands second makes this PR's change to its type there.It also shares lines with #431 (
native/api.tsx), #459 (native/conditions/media-query.ts); whichever lands second rebases.Base
Re-written on
main(a5002c5), which installs React Native 0.86.3; this PR was written against 0.81.4. The installed-module suites now run 0.86'sAppearance, so the suite that transcribed 0.86 is gone, and the census covers the 0.81 rule.mainmaps"unspecified"tonullin the observable, which this keeps.