Skip to content

React Native Architecture Issue: Provider Contains Too Much State, Causing Unnecessary Rerenders #341

@coryetzkorn

Description

@coryetzkorn

Problem

ElevenLabsProvider contains 8+ pieces of frequently-changing state, causing all children to rerender on every state change. This is a fundamental architectural issue that can't be fully fixed with memoization alone. I tried to fix it in #340 but it's still not a full fix.

Unlike other React Native libraries (see PostHog, React Query, Redux), ElevenLabsProvider puts call state inside the provider component, which causes performance issues.

Current Architecture (Problematic)

export const ElevenLabsProvider = ({ children }) => {
  // ❌ These state changes cause provider to rerender
  const [token, setToken] = useState('');
  const [connect, setConnect] = useState(false);
  const [status, setStatus] = useState('disconnected');      // Changes during call
  const [conversationId, setConversationId] = useState('');
  const [isSpeaking, setIsSpeaking] = useState(false);       // Toggles constantly
  const [canSendFeedback, setCanSendFeedback] = useState(false);
  const [serverUrl, setServerUrl] = useState(DEFAULT_SERVER_URL);
  const [tokenFetchUrl, setTokenFetchUrl] = useState(undefined);
  
  // When any state changes, provider rerenders → all children rerender
  return (
    <ElevenLabsContext.Provider value={contextValue}>
      <LiveKitRoomWrapper {...props}>
        {children}  {/* ← Rerenders on every state change */}
      </LiveKitRoomWrapper>
    </ElevenLabsContext.Provider>
  );
};

Every setState call causes the provider to rerender, which causes all children to rerender by default in React.

Example: PostHog's Approach (Better)

PostHog's PostHogProvider can wrap an entire app without performance issues because it doesn't have internal state:

// PostHog's approach (simplified)
export function PostHogProvider({ children, apiKey, options }) {
  // ✅ Creates client ONCE on mount - stable reference
  const client = useMemo(() => new PostHog(apiKey, options), [apiKey]);
  
  // ✅ Context value is stable - doesn't change during usage
  const value = useMemo(() => ({ client }), [client]);
  
  // ✅ No state = no rerenders = wrapping entire app is fine
  return (
    <PostHogContext.Provider value={value}>
      {children}
    </PostHogContext.Provider>
  );
}

PostHog can wrap an entire app because the provider never rerenders.

Proposed Architecture (Following PostHog's Pattern)

// Provider only provides stable client instance (no state)
export function ElevenLabsProvider({ children }) {
  // ✅ Client is stable - doesn't change
  const client = useMemo(() => new ElevenLabsClient(), []);
  
  return (
    <ElevenLabsContext.Provider value={client}>
      {children}
    </ElevenLabsContext.Provider>
  );
}

// State managed by consumers via hooks
export function useConversation(options) {
  const client = useContext(ElevenLabsContext);
  
  // ✅ State is local to the component using the hook
  const [status, setStatus] = useState('disconnected');
  const [isSpeaking, setIsSpeaking] = useState(false);
  const [canSendFeedback, setCanSendFeedback] = useState(false);
  
  // ✅ Only components using this hook rerender when state changes
  useEffect(() => {
    // Setup LiveKit connection using client
    // Update local state as needed
  }, [client]);
  
  return { 
    status, 
    isSpeaking, 
    canSendFeedback,
    startSession, 
    endSession,
    // ...
  };
}

Benefits

  • Provider never rerenders (no state changes)
  • Only components using the hook manage state (isolated rerenders)
  • Can wrap entire app without performance concerns
  • Better separation of concerns (provider = client, hook = state)
  • Follows React best practices and patterns used by successful libraries

Comparison to Other Libraries

Library Provider Has State? Can Wrap Entire App?
PostHog ❌ No ✅ Yes
React Query ❌ No ✅ Yes
Redux ❌ No ✅ Yes
ElevenLabs ✅ Yes (8+ pieces) ❌ No (performance issues)

Current Workarounds

Developers currently have to:

  1. Only wrap minimal UI components with ElevenLabsProvider (can't wrap entire app)
  2. Use React.memo on direct children to prevent rerenders
  3. Apply patches to memoize context values (see PR Prevent Unnecessary Rerenders in ElevenLabsProvider (React Native) #340)

References

Would love to see ElevenLabs adopt this pattern - it would make the library much more ergonomic and performant! 🚀

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions