skip to content

In React Native's AccessibilityInfo, what happens when code queries or subscribes to a setting the current platform does not support?

level: middleimportance: should knowfreq 20%

answer

  1. false can mean not available
  2. iOS-only queries resolve false on Android
  3. unmapped events: remove() that does nothing
  4. isAccessibilityServiceEnabled rejects on iOS
  5. touch exploration versus any service

basics

~10 s

Mostly nothing visible: an iOS-only query such as isBoldTextEnabled resolves to false on Android, an unmapped event returns an inert subscription, and isAccessibilityServiceEnabled rejects on iOS, so false often means unsupported rather than off.

solid answer

~40 s

React Native keeps one `AccessibilityInfo` surface for two platforms whose settings do not match, and it papers over the gaps quietly. iOS-only queries such as `isBoldTextEnabled()`, `isReduceTransparencyEnabled()` and `prefersCrossFadeTransitions()` resolve to `false` on Android without asking the system; `isHighTextContrastEnabled()` resolves to `false` on iOS. `addEventListener` with an event the platform does not map, such as `boldTextChanged` on Android, returns a subscription whose `remove()` does nothing, and the handler never runs. The exception is `isAccessibilityServiceEnabled()`, which rejects on iOS. So I treat a platform-only `false` as unknown, branch on `Platform.OS` before calls that reject, and test each adaptation with the real setting on both platforms.

code

tsx · 32 lines
tsx
import {useEffect, useState} from 'react';
import {AccessibilityInfo, Platform} from 'react-native';

export function useAnyAccessibilityService(): boolean {
  const [enabled, setEnabled] = useState(false);

  useEffect(() => {
    if (Platform.OS !== 'android') {
      return;
    }
    let active = true;
    const subscription = AccessibilityInfo.addEventListener(
      'accessibilityServiceChanged',
      value => {
        setEnabled(value);
      },
    );
    AccessibilityInfo.isAccessibilityServiceEnabled()
      .then(value => {
        if (active) {
          setEnabled(value);
        }
      })
      .catch(() => {});
    return () => {
      active = false;
      subscription.remove();
    };
  }, []);

  return enabled;
}

go deeper

for a junior

Recall that some AccessibilityInfo queries are iOS-only or Android-only, and that the screen-reader and reduce-motion checks work on both platforms.

for a middle

Explain the three behaviours on the unsupported platform: a false resolution, a rejection from isAccessibilityServiceEnabled on iOS, and an inert subscription for unmapped events.

for a senior

Show how you make platform-only flags explicit in hook names, branch before calls that reject, and test each adaptation with the real setting on both platforms.

for a principal

Discuss how a shared accessibility layer should expose platform-only settings so feature teams cannot mistake a missing capability for a user preference.

## Why this matters `AccessibilityInfo` is React Native's module for reading device accessibility settings. It exposes one JavaScript surface for two platforms whose settings do not line up one to one: iOS has a Bold Text switch and Reduce Transparency, while Android exposes high text contrast and an accessibility timeout. React Native mostly does **not** throw when you ask about a setting the platform lacks; it answers with a harmless default. The consequence is that **a `false` can mean "off" or "not available here"**, and code that treats the two the same ships adaptations that silently never run on one platform. ## What each kind of call does **Queries for an iOS-only setting, called on Android, resolve to `false` without asking the system:** - `isBoldTextEnabled()` - `isDarkerSystemColorsEnabled()` - `isReduceTransparencyEnabled()` - `prefersCrossFadeTransitions()` **Android-only calls, made on iOS:** - `isHighTextContrastEnabled()` resolves to `false`. - `getRecommendedTimeoutMillis(originalTimeout)` resolves to the `originalTimeout` you passed in. - `isAccessibilityServiceEnabled()` is the exception: it **rejects** with an error saying it is only available on Android. **Events the current platform does not map.** `addEventListener` looks the event name up in a per-platform table. When there is no entry, it returns a subscription object whose `remove()` does nothing, and the handler is never called. No error, no warning. | Event | Fires on | On the other platform | |---|---|---| | `boldTextChanged` | iOS | Android: inert subscription | | `reduceTransparencyChanged` | iOS | Android: inert subscription | | `announcementFinished` | iOS | Android: inert subscription | | `highTextContrastChanged` | Android | iOS: inert subscription | | `accessibilityServiceChanged` | Android | iOS: inert subscription | | `screenReaderChanged`, `reduceMotionChanged` | both | not applicable | Separately, most queries **reject** when the native module they rely on is missing, so a `catch` belongs on every query promise. ## Screen reader versus any service on Android Android has two related checks, and they answer different questions: - `isScreenReaderEnabled()` reports whether **touch exploration** is on, the mode TalkBack switches on so that touching the screen reads what is under the finger. - `isAccessibilityServiceEnabled()` reports whether **any** accessibility service is enabled: TalkBack, but also any third-party accessibility app the user installed. Use the first for adaptations aimed at spoken output, such as keeping a toast visible longer. The second is broader and can be `true` while no screen reader is running, so using it as a screen-reader check over-triggers. ## Writing code that survives the gaps 1. **Branch on `Platform.OS` before calling an API whose unsupported-platform behaviour is a rejection**, such as `isAccessibilityServiceEnabled()`. 2. **Treat `false` from a platform-only query as "unknown or off"**, and never build a feature that only works when it is `true` without deciding what the other platform does. 3. **Attach `catch` to every query** and fall back to a sensible default. 4. **Name platform-only hooks honestly**, for example `useIOSBoldText()`, so a caller cannot mistake a missing capability for a user preference. 5. **Test with the real setting toggled on both platforms.** A unit test that mocks `isBoldTextEnabled()` to `true` proves nothing about Android, where the real call never returns `true`. ## A worked example A transfer screen thickens the strokes of its amount field when bold text is on. On iOS the hook follows the user's setting. On Android `isBoldTextEnabled()` is always `false` and `boldTextChanged` never fires, so the heavier style never applies, and nothing in the logs says why. The fix is not a workaround inside the hook; it is a decision about what the app should do on Android, where this API has no answer, made before a user reports it. ## Summary | Call on the unsupported platform | Result | |---|---| | iOS-only query on Android | resolves `false` | | `isHighTextContrastEnabled()` on iOS | resolves `false` | | `getRecommendedTimeoutMillis(t)` on iOS | resolves `t` | | `isAccessibilityServiceEnabled()` on iOS | rejects | | unmapped event | inert subscription |

  • When would you use isAccessibilityServiceEnabled instead of isScreenReaderEnabled on Android?
    Rarely: when an adaptation should apply to any accessibility service, not only TalkBack, such as pausing an auto-advancing carousel. For adaptations aimed at spoken output use `isScreenReaderEnabled()`, which reports touch exploration. The broader check can be `true` with no screen reader running, and it rejects on iOS, so it needs a `Platform.OS` branch.
  • How does getRecommendedTimeoutMillis fit in, and what does it return on iOS?
    On Android it returns the timeout the user needs according to the accessibility timeout setting, or the value you pass when none is set; use it for how long a toast or snackbar stays visible. On iOS it simply resolves to the value you passed in, so the call is safe on both platforms.

saying these in an interview costs you the question

  • isBoldTextEnabled throws on Android, so a try/catch reveals the platform gap.
  • A false from isBoldTextEnabled on Android proves the user has no bold-text preference.
  • Subscribing to boldTextChanged on Android throws an unsupported-event error.
  • isAccessibilityServiceEnabled is just another name for isScreenReaderEnabled on Android.
  • Every platform-only AccessibilityInfo query resolves to false on the other platform.