Brownie: A Type-Safe Shared-State Workflow for Brownfield React Native

Authors

Mixing two technologies that allow for the same outcomes, such as React Native and Swift or Kotlin, creates a challenge: shall we allow the data to live duplicated over two realms, or create a single source of truth with automatic synchronization. Spoiler alert: whenever possible pick the latter.

When we introduced Brownie back in January, the goal was clear: make shared state in brownfield React Native apps predictable, type-safe, and practical for production teams by having a single source of truth that is always in-sync between native and JavaScript threads. We started with iOS to experiment whether the idea is sound.

Today we’re introducing Android support, making Brownie ready for cross-platform use in iOS and Android host apps.

Getting started with Brownie

Brownie enforces not only runtime synchronization, but also build-time guarantees for store shape through code generation. That’s why we start with a single source of truth: a TypeScript store schema in a *.brownie.ts file.

import type { BrownieStore } from "@callstack/brownie";

interface AppStore extends BrownieStore {
  counter: number;
}

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

From there, brownfield codegen command, that comes with React Native Brownfield library, generates native store types, and your UI layers consume the same shared state with platform-native APIs.

React Native (TypeScript)

On the React Native side, you can use useStore with selectors to get access to that store, and get updates focused, type-safe, re-rendering React state only when necessary:

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

const [counter, setState] = useStore(
  "AppStore",
  (s) => s.counter,
);

function handlePress() {
  setState((prev) => ({
    counter: prev.counter + 1,
  }));
}

iOS (SwiftUI/UIKit)

On the native iOS side, all you need to do is to register the store once with the initial state, and then read and update using SwiftUI's @UseStore or UIKit subscriptions:

import Brownie
import ReactBrownfield

// register the store
AppStore.register(AppStore(counter: 0))

Then update the store as needed using SwiftUI’s @UseStore directive:

import Brownie
import ReactBrownfield

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

  var body: some View {
    VStack {
      Text("Count: \(Int(counter))")
      Button("Increment") {
        $counter.set { $0 + 1 }
      }
    }
  }
}

The @UseStore and AppStore are included with the artifacts packaged by the Brownfield CLI.

Android (Kotlin)

On the native Android side, we need to register the store during startup like this:

import com.callstack.brownie.registerStoreIfNeeded

registerStoreIfNeeded(storeName = AppStore.STORE_NAME) {
  AppStore(counter = 0.0)
}

Then update the store as required using subscribe and store.set:

import com.brownfield.example.AppStore
import com.callstack.brownie.Store
import com.callstack.brownie.StoreManager
import com.callstack.brownie.store
import com.callstack.brownie.subscribe

private fun appStore(): Store<BrownfieldStore>? {
    return StoreManager.shared.store(AppStore.STORE_NAME)
}

@Composable
fun GreetingCard() {
    var counter by remember { mutableIntStateOf(0) }

    DisposableEffect(Unit) {
        val store = appStore()
        val unsubscribe = store?.subscribe(
            selector = { state -> state.counter.toInt() },
            onChange = { updatedCounter -> counter = updatedCounter }
        ) ?: {}

        onDispose {
            unsubscribe()
        }
    }

    Column {
        Text( text = "Count $counter")
        Button(onClick = {
            brownieStore()?.set { state ->
                state.copy(counter = state.counter + 1)
            }
        }) {
            Text("Increment counter")
        }
    }
}

The APIs in import statements are packaged together with the artifacts generated by the brownfield CLI.

Android usage supports:

  • Easy Path (recommended): generated types with Gson-based serialization
  • Advanced Path: custom serializers for compatibility rules, custom formats, or encrypted payloads

How it all works End-to-End

You’ve seen that Brownie relies on code generation, and (not surprisingly) requires you to write some code in TypeScript, Kotlin, and Swift. Let us quickly show you how this all fits together in a full pipeline:

  1. Define store schema in *.brownie.ts file
  2. Run Brownfield packaging and codegen for your target platform artifacts
  3. Register store in native app startup (Swift/Kotlin)
  4. Read and mutate state from React Native and native UI layers
  5. Subscribe to selected state slices for reactive updates

This keeps state updates consistent across the embedded React Native surface and native host UI on both platforms.

Migration from alpha versions

What's new since Alpha

The main update is a more mature, end-to-end workflow that offers:

  • Stable APIs ready for production Brownfield apps
  • Android support alongside iOS, including generated Kotlin types
  • A clearer setup path from schema definition to native registration
  • Kotlin integration with both an Easy Path and an Advanced Path

Brownie has moved past an iOS-first phase and now provides one state bridge across React Native, Swift, and Kotlin.

Migrating from Alpha

If you evaluated Brownie during the alpha phase, the main shift is moving from an experimental, iOS-first setup to a stable, cross-platform approach.

As an alpha user you should already have Brownie configured for iOS.

The setup flow is more cohesive now. Rather than stitching together early examples, it’s best to follow the current Getting Started guide, which reflects the unified onboarding and packaging process. The API surface has matured as well. Instead of relying on early patterns, use the documented APIs, useStore, native registration helpers, and typed store access, which are stable and consistent across platforms.

With that in mind, enabling Android support is straightforward:

  • Update package.json

In the below code snippet, replace rnapp with your react-native app identifier and brownfieldlib with your Brownfield Android module name:

"brownie": {
  "kotlin": "./android/BrownfieldLib/src/main/java/com/rnapp/brownfieldlib/Generated/",
  "kotlinPackageName": "com.rnapp.brownfieldlib"
},
  • Add Gson dependency

In your Brownfield Android module, add the Gson dependency:

api("com.google.code.gson:gson:2.13.1")

That is all you need to do. Your already existing Brownie store, which you previously could only use for iOS is now ready to be used in Android.

// package AAR

npx brownfield package:android --module-name :YourModuleName --variant release

// Publish AAR

npx brownfield publish:android --module-name :YourModuleName
For further documentation, head over to getting started with Brownie.

If your existing store schema already lives in *.brownie.ts, migration is mostly about updating platform integration steps and rebuilding artifacts with the current CLI flow.

How this impacts Brownfield integrations

The success of migrating from one technology stack to another depends on reducing accidental complexity at integration boundaries. Brownie helps with that by providing a single schema contract across React Native and native code, generating type-safe models in Swift and Kotlin, and maintaining a consistent state synchronization model regardless of platform.

That means less glue code, fewer runtime mismatches, and a smoother path for incremental migration.

If you’re integrating React Native into an existing native app today, this is a strong point to adopt Brownie: stable APIs, iOS and Android support, and a setup flow designed for production teams. Head over to the documentation to dive further.

And to make migrations and creating PoCs of React Native running side-by-side your native apps easier for you, we’ve recently released agent skills for React Native Brownfield library.

npx skills add callstackincubator/agent-skills --skill react-native-brownfield-migration

Just tell your agent to use the skill and give React Native a try in a matter of minutes.

Table of contents
Adding React Native to existing apps?

We help teams introduce React Native into brownfield projects effectively.

Let’s chat

//

//
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.

//
Insights

Learn more about Brownfield

Here's everything we published recently on this topic.