fix(mobile): read device type, onboarding root and player atoms written during mount

jotai 3 subscribes after the mount commit, so a value written in between
is missed until the next write. The device type arrives from an async
init while the first screens mount, the onboarding check reads routes
that screens push from mount effects, and every mounted player control
writes the pause flag and playback rate in its own mount effect, so a
sibling mounting in the same commit kept stale values. Read them with
useAtomValueRawSync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DIYgod
2026-10-12 08:19:12 +08:00
parent 6c2b46693e
commit 6e612963cb
3 changed files with 13 additions and 7 deletions
+3 -2
View File
@@ -1,10 +1,11 @@
import { jotaiStore } from "@follow/utils"
import { useAtomValue } from "jotai"
import { useAtomValueRawSync } from "jotai"
import { appAtoms } from "../app"
export const useDeviceType = () => {
const deviceType = useAtomValue(appAtoms.deviceType)
// Set by an async init that can land while the first screens mount
const deviceType = useAtomValueRawSync(appAtoms.deviceType)
return deviceType
}
+2 -2
View File
@@ -1,5 +1,5 @@
import { useIsNewUser, useWhoami } from "@follow/store/user/hooks"
import { useAtomValue } from "jotai"
import { useAtomValueRawSync } from "jotai"
import { selectAtom } from "jotai/utils"
import { useEffect, useMemo, useRef } from "react"
@@ -13,7 +13,7 @@ export function useOnboarding() {
const navigation = useNavigation()
// The sign-in screen pops to the root shortly after signing in, which would also close an
// onboarding presented in the meantime, so it waits until nothing is open over the tabs
const isAtRoot = useAtomValue(
const isAtRoot = useAtomValueRawSync(
useMemo(
() => selectAtom(navigation.__dangerous_getCtxValue().routesAtom, (r) => r.length === 0),
[navigation],
+8 -3
View File
@@ -6,7 +6,7 @@ import TrackPlayer, {
usePlaybackState,
} from "@rntp/player"
import { t } from "i18next"
import { atom, useAtom } from "jotai"
import { atom, useAtomValueRawSync, useSetAtom } from "jotai"
import { useCallback, useEffect, useSyncExternalStore } from "react"
import { PlayerRegistered } from "../initialize/player"
@@ -20,7 +20,10 @@ const pausedByUserAtom = atom(false)
function usePlaybackStatus() {
const playing = useNativeIsPlaying()
const playbackState = usePlaybackState()
const [pausedByUser, setPausedByUser] = useAtom(pausedByUserAtom)
// Every mounted player control resets it in an effect, so a sibling mounting in the same
// commit must not keep the value it rendered with
const pausedByUser = useAtomValueRawSync(pausedByUserAtom)
const setPausedByUser = useSetAtom(pausedByUserAtom)
useEffect(() => {
if (playing) setPausedByUser(false)
@@ -159,7 +162,9 @@ export type Rate = (typeof allowedRate)[number]
const rateAtom = atom<Rate>(1)
export function useRate() {
const [rate, setRate] = useAtom(rateAtom)
// Synced from the native player in a mount effect, see usePlaybackStatus
const rate = useAtomValueRawSync(rateAtom)
const setRate = useSetAtom(rateAtom)
useEffect(() => {
function getRate() {