Introducing Brownie: Type-Safe Shared State for React Native Brownfield Apps

Authors
No items found.

We're excited to announce the alpha release of Brownie (@callstack/brownie), a shared state management library designed specifically for React Native brownfield applications.

The Problem

Brownfield apps are existing native applications with React Native integration that face a fundamental challenge: shared state.

Your native settings screen shows the user's profile. Your React Native checkout flow needs that same user data. An authentication token refreshed in Swift needs to be available in React Native immediately. Theme preferences changed in one place should reflect everywhere.

From our experience working with clients, we've noticed a recurring pattern: every app ends up building its own proprietary Turbo Module for data sharing. Each team reinvents the wheel: writing custom native modules, maintaining type definitions on both sides, and constantly updating these modules as requirements change.

One of the examples is the Brownfield integration at Zalando. You can read about it here.

This is the main pain point of brownfield development today. These homegrown solutions are:

  • Time-consuming to build: Writing Turbo Modules requires native expertise on each platform.
  • Tedious to maintain: Every schema change means updating TypeScript, Swift, and Kotlin.
  • Hard to keep in sync: Types drift apart, leading to runtime crashes.
  • Duplicated across the industry: Every brownfield team solves the same problem from scratch.

Our Approach

Brownie takes a different path. Instead of treating React Native and native code as separate worlds, we created a single source of truth accessible from both TypeScript and Swift.

One key design decision is that state lives on the native side. This means your native app can read and write to the store independently of React Native. React Native can start in its own time. When it's ready, it will pick up the current state automatically.

The architecture is straightforward:

  1. Define your state shape once in TypeScript
  2. Generate native types automatically via CLI
  3. Access the same state from both React Native and Swift
  4. Always retrieve up to date state thanks to bi directional sync

Under the hood, state lives in a C++ layer, exposed to JavaScript via JSI and to Swift via an Objective-C++ bridge.

How It Works

1. Define Your Store

Create a *.brownie.ts file with your state shape:

// AppStore.brownie.ts
import type { BrownieStore } from '@callstack/brownie';

interface AppStore extends BrownieStore {
  counter: number;
  user: {
    name: string;
    email: string;
  };
  settings: {
    theme: 'light' | 'dark';
    notificationsEnabled: boolean;
  };
}

declare module '@callstack/brownie' {
  interface BrownieStores {
    AppStore: AppStore;
  }
}

2. Build XCFrameworks

The brownfield CLI handles codegen and packaging in one step:

npx brownfield package:ios --scheme YourScheme --configuration Release

This generates Swift types from your .brownie.ts files and builds everything into XCFrameworks:

  • YourScheme.xcframework - Your React Native module
  • Brownie.xcframework - Shared state library
  • ReactBrownfield.xcframework - Brownfield integration
  • hermesvm.xcframework - JavaScript engine

Drag these into your native Xcode project and set to Embed & Sign.

The generated Swift structs match your TypeScript schema exactly:

// Generated automatically
struct AppStore: Codable, Equatable {
    var counter: Double
    var user: User
    var settings: Settings
}

struct User: Codable, Equatable {
    var name: String
    var email: String
}

Note: Generated code is already included in Brownie.xcframework

3. Use in React Native

The useStore hook provides familiar React patterns with selector support:

import { useStore } from '@callstack/brownie';

function Counter() {
  const [counter, setState] = useStore('AppStore', (s) => s.counter);

  return (
    <Button
      title={`Count: ${counter}`}
      onPress={() => setState((prev) => ({ counter: prev.counter + 1 }))}
    />
  );
}

Selectors ensure components only re-render when their selected slice changes, just like you'd expect from a modern state library.

4. Use in SwiftUI

The @UseStore property wrapper brings the same ergonomics to SwiftUI:

import Brownie

struct CounterView: View {
  @UseStore(\\AppStore.counter) var counter

  var body: some View {
    VStack {
      Text("Count: \\(Int(counter))")

      Button("Increment") {
        $counter.set { $0 + 1 }
      }
    }
  }
}

KeyPath selectors provide type-safe access and optimal re-renders. The projected value ($counter) returns a standard SwiftUI Binding, so it works seamlessly with built-in controls like Stepper, Slider, and TextField.

5. UIKit Support

For UIKit apps, use the subscription-based API:

class CounterViewController: UIViewController {
  private var store: Store<AppStore>?
  private var cancelSubscription: (() -> Void)?

  override func viewDidLoad() {
    super.viewDidLoad()
    store = StoreManager.get(key: AppStore.storeName, as: AppStore.self)

    cancelSubscription = store?.subscribe(\\.counter) { [weak self] counter in
      self?.updateLabel(counter)
    }
  }

  deinit {
    cancelSubscription?()
  }
}

Why We Built This

At Callstack, we work with many teams integrating React Native into existing native apps. After seeing the same pattern repeat (teams building custom Turbo Modules for data sharing, then spending ongoing effort maintaining them), we decided to solve this once and for all.

Brownie replaces those proprietary data-sharing modules with a standardized solution. Instead of writing and maintaining your own native code for state synchronization, you define your schema in TypeScript and let Brownie handle the rest.

We wanted something that:

  • Eliminates custom native modules: No more writing Turbo Modules just to share data.
  • Feels native on both sides: React hooks in JS, property wrappers in Swift.
  • Is type-safe end-to-end: TypeScript schema generates Swift types automatically.
  • Stays in sync automatically: One schema, generated types, no manual updates.

Current Status: Alpha (iOS Only)

This is an early alpha release. We're shipping iOS support first to gather feedback and iterate on the API before expanding to Android.

What works today:

  • SwiftUI integration with @UseStore property wrapper
  • UIKit integration with subscribe-based API
  • React Native useStore hook with selectors
  • Multiple stores support
  • Nested objects and complex types
  • Automatic codegen for Swift types
  • XCFramework packaging via CLI

Known limitations:

  • iOS only (Android coming soon)
  • Full state sync on updates (partial sync optimization planned)
  • APIs may change based on feedback

We Want Your Feedback

This is an early alpha release and we're actively seeking feedback. If you try Brownie, let us know what works, what doesn't, and what's missing for your use case.

Open an issue on GitHub with your thoughts.

Brownie is part of the React Native Brownfield project. Check out the documentation for detailed guides on store definitions, codegen configuration, and platform-specific usage.

Table of contents
Adding React Native to existing apps?

We help teams introduce React Native into brownfield projects effectively.

Let’s chat

//

//
Insights

Learn more about Brownfield

Here's everything we published recently on this topic.

//
Brownfield

We can help you move
it forward!

At Callstack, we work with companies big and small, pushing React Native everyday.

New Architecture Migration

Safely migrate to React Native’s New Architecture to unlock better performance, new capabilities, and future-proof releases.

Code Sharing

Implement effective code-sharing strategies across all platforms to accelerate shipping and reduce code duplication.

Migration to React Native

Plan and execute a migration from native or hybrid stacks to React Native with minimal disruption and clear technical direction.

React Native Brownfield

Integrate React Native into existing native application and start sharing code across platforms immediately.