Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions apps/TesterIntegrated/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect } from 'react';
import { StyleSheet, Text, View, Button, TextInput } from 'react-native';
import { useBrownieStore } from '@callstack/brownie';
import { useStore } from '@callstack/brownie';
import {
createNativeStackNavigator,
type NativeStackScreenProps,
Expand Down Expand Up @@ -34,7 +34,8 @@ const theme = getRandomTheme();

function HomeScreen({ navigation, route }: HomeScreenProps) {
const colors = route.params?.theme || theme;
const [state, setState] = useBrownieStore('BrownfieldStore');
const [counter, setState] = useStore('BrownfieldStore', (s) => s.counter);
const [user] = useStore('BrownfieldStore', (s) => s.user);

useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
Expand All @@ -51,12 +52,12 @@ function HomeScreen({ navigation, route }: HomeScreenProps) {
</Text>

<Text style={[styles.text, { color: colors.secondary }]}>
Count: {state.counter}
Count: {counter}
</Text>

<TextInput
style={styles.input}
value={state.user.name}
value={user.name}
onChangeText={(text) =>
setState((prev) => ({ user: { ...prev.user, name: text } }))
}
Expand Down
24 changes: 22 additions & 2 deletions packages/brownie/ArchitectureOverview.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,8 +308,8 @@ packages/brownie/
### React Hook

```ts
// useState-like API for store access
const [state, setState] = useBrownieStore('BrownfieldStore');
// Full state (useState-like API)
const [state, setState] = useStore('BrownfieldStore');

// Read state
console.log(state.counter);
Expand All @@ -321,6 +321,26 @@ setState({ counter: state.counter + 1 });
setState((prev) => ({ counter: prev.counter + 1 }));
```

### Selectors

Selectors allow subscribing to a slice of state, reducing unnecessary re-renders:

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

// Select primitive - re-renders only when counter changes
const [counter, setState] = useStore('BrownfieldStore', (s) => s.counter);

// Select object
const [user, setState] = useStore('BrownfieldStore', (s) => s.user);
```

**How it works:**

1. Native side notifies JS on every state change (full state)
2. Each `useStore` subscribes to full state but extracts only what it needs via selector
3. React's `useSyncExternalStore` compares previous vs new selected value - skips re-render if equal

### Core Functions

```ts
Expand Down
53 changes: 41 additions & 12 deletions packages/brownie/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,45 @@
/** @type {import('jest').Config} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['<rootDir>/scripts/**/*.test.ts'],
moduleFileExtensions: ['ts', 'js'],
clearMocks: true,
transform: {
'^.+\\.ts$': [
'ts-jest',
{
tsconfig: '<rootDir>/scripts/__tests__/tsconfig.json',
projects: [
{
displayName: 'scripts',
testEnvironment: 'node',
testMatch: ['<rootDir>/scripts/**/*.test.ts'],
moduleFileExtensions: ['ts', 'js'],
clearMocks: true,
transform: {
'^.+\\.ts$': [
'babel-jest',
{
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
'@babel/preset-typescript',
],
},
],
},
],
},
},
{
displayName: 'src',
testEnvironment: 'node',
testMatch: ['<rootDir>/src/**/*.test.ts'],
moduleFileExtensions: ['ts', 'js'],
clearMocks: true,
moduleNameMapper: {
'^./NativeBrownieModule$':
'<rootDir>/src/__tests__/__mocks__/NativeBrownieModule.ts',
},
transform: {
'^.+\\.ts$': [
'babel-jest',
{
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
'@babel/preset-typescript',
],
},
],
},
},
],
};
2 changes: 1 addition & 1 deletion packages/brownie/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.3",
"@babel/preset-typescript": "^7.27.1",
"@babel/runtime": "^7.25.0",
"@react-native/babel-preset": "0.82.1",
"@react-native/eslint-config": "0.82.1",
Expand All @@ -78,7 +79,6 @@
"react": "19.1.1",
"react-native": "0.82.1",
"react-native-builder-bob": "^0.40.14",
"ts-jest": "^29.2.5",
"typescript": "5.8.3"
},
"codegenConfig": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default {
nativeStoreDidChange: jest.fn(),
};
6 changes: 6 additions & 0 deletions packages/brownie/src/__tests__/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "../../../../tsconfig",
"compilerOptions": {
"verbatimModuleSyntax": false
}
}
45 changes: 38 additions & 7 deletions packages/brownie/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useSyncExternalStore } from 'react';
import { useCallback, useDebugValue, useSyncExternalStore } from 'react';
import BrownieModule from './NativeBrownieModule';

/**
Expand Down Expand Up @@ -95,23 +95,54 @@ export function setState<K extends keyof BrownieStores>(
}
}

const identity = <T>(x: T): T => x;

/**
* React hook for subscribing to a native store.
* React hook for subscribing to a native store with optional selector.
* Inspired by Zustand's useStore implementation.
* @param key Store key registered in StoreManager
* @returns Tuple of [state, setState] for the store
*/
export function useBrownieStore<K extends keyof BrownieStores>(
export function useStore<K extends keyof BrownieStores>(
key: K
): [BrownieStores[K], (action: SetStateAction<BrownieStores[K]>) => void] {
): [BrownieStores[K], (action: SetStateAction<BrownieStores[K]>) => void];

/**
* React hook for subscribing to a native store with selector.
* Inspired by Zustand's useStore implementation.
* @param key Store key registered in StoreManager
* @param selector Function to select a slice of state
* @returns Tuple of [selectedState, setState] for the store
*/
export function useStore<K extends keyof BrownieStores, U>(
key: K,
selector: (state: BrownieStores[K]) => U
): [U, (action: SetStateAction<BrownieStores[K]>) => void];

export function useStore<K extends keyof BrownieStores, U>(
key: K,
selector?: (state: BrownieStores[K]) => U
): [U | BrownieStores[K], (action: SetStateAction<BrownieStores[K]>) => void] {
const sub = useCallback(
(listener: () => void) => subscribe(key, listener),
[key]
);
const snap = useCallback(() => getSnapshot(key), [key]);
const state = useSyncExternalStore(sub, snap, snap);
const snap = useCallback(
() =>
(selector ?? (identity as (state: BrownieStores[K]) => U))(
getSnapshot(key)
),
[key, selector]
);

const slice = useSyncExternalStore(sub, snap, snap);

useDebugValue(slice);

const boundSetState = useCallback(
(action: SetStateAction<BrownieStores[K]>) => setState(key, action),
[key]
);
return [state, boundSetState];

return [slice, boundSetState];
}
73 changes: 5 additions & 68 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1458,7 +1458,7 @@ __metadata:
languageName: node
linkType: hard

"@babel/preset-typescript@npm:^7.24.7":
"@babel/preset-typescript@npm:^7.24.7, @babel/preset-typescript@npm:^7.27.1":
version: 7.28.5
resolution: "@babel/preset-typescript@npm:7.28.5"
dependencies:
Expand Down Expand Up @@ -1601,6 +1601,7 @@ __metadata:
dependencies:
"@babel/core": "npm:^7.25.2"
"@babel/preset-env": "npm:^7.25.3"
"@babel/preset-typescript": "npm:^7.27.1"
"@babel/runtime": "npm:^7.25.0"
"@react-native/babel-preset": "npm:0.82.1"
"@react-native/eslint-config": "npm:0.82.1"
Expand All @@ -1613,7 +1614,6 @@ __metadata:
react: "npm:19.1.1"
react-native: "npm:0.82.1"
react-native-builder-bob: "npm:^0.40.14"
ts-jest: "npm:^29.2.5"
ts-morph: "npm:^25.0.0"
typescript: "npm:5.8.3"
peerDependencies:
Expand Down Expand Up @@ -5388,15 +5388,6 @@ __metadata:
languageName: node
linkType: hard

"bs-logger@npm:^0.2.6":
version: 0.2.6
resolution: "bs-logger@npm:0.2.6"
dependencies:
fast-json-stable-stringify: "npm:2.x"
checksum: 10/e6d3ff82698bb3f20ce64fb85355c5716a3cf267f3977abe93bf9c32a2e46186b253f48a028ae5b96ab42bacd2c826766d9ae8cf6892f9b944656be9113cf212
languageName: node
linkType: hard

"bser@npm:2.1.1":
version: 2.1.1
resolution: "bser@npm:2.1.1"
Expand Down Expand Up @@ -7529,7 +7520,7 @@ __metadata:
languageName: node
linkType: hard

"fast-json-stable-stringify@npm:2.x, fast-json-stable-stringify@npm:^2.0.0, fast-json-stable-stringify@npm:^2.1.0":
"fast-json-stable-stringify@npm:^2.0.0, fast-json-stable-stringify@npm:^2.1.0":
version: 2.1.0
resolution: "fast-json-stable-stringify@npm:2.1.0"
checksum: 10/2c20055c1fa43c922428f16ca8bb29f2807de63e5c851f665f7ac9790176c01c3b40335257736b299764a8d383388dabc73c8083b8e1bc3d99f0a941444ec60e
Expand Down Expand Up @@ -8153,7 +8144,7 @@ __metadata:
languageName: node
linkType: hard

"handlebars@npm:^4.7.7, handlebars@npm:^4.7.8":
"handlebars@npm:^4.7.7":
version: 4.7.8
resolution: "handlebars@npm:4.7.8"
dependencies:
Expand Down Expand Up @@ -10358,13 +10349,6 @@ __metadata:
languageName: node
linkType: hard

"lodash.memoize@npm:^4.1.2":
version: 4.1.2
resolution: "lodash.memoize@npm:4.1.2"
checksum: 10/192b2168f310c86f303580b53acf81ab029761b9bd9caa9506a019ffea5f3363ea98d7e39e7e11e6b9917066c9d36a09a11f6fe16f812326390d8f3a54a1a6da
languageName: node
linkType: hard

"lodash.merge@npm:4.6.2, lodash.merge@npm:^4.6.2":
version: 4.6.2
resolution: "lodash.merge@npm:4.6.2"
Expand Down Expand Up @@ -10525,7 +10509,7 @@ __metadata:
languageName: node
linkType: hard

"make-error@npm:^1.1.1, make-error@npm:^1.3.6":
"make-error@npm:^1.1.1":
version: 1.3.6
resolution: "make-error@npm:1.3.6"
checksum: 10/b86e5e0e25f7f777b77fabd8e2cbf15737972869d852a22b7e73c17623928fccb826d8e46b9951501d3f20e51ad74ba8c59ed584f610526a48f8ccf88aaec402
Expand Down Expand Up @@ -14643,46 +14627,6 @@ __metadata:
languageName: node
linkType: hard

"ts-jest@npm:^29.2.5":
version: 29.4.6
resolution: "ts-jest@npm:29.4.6"
dependencies:
bs-logger: "npm:^0.2.6"
fast-json-stable-stringify: "npm:^2.1.0"
handlebars: "npm:^4.7.8"
json5: "npm:^2.2.3"
lodash.memoize: "npm:^4.1.2"
make-error: "npm:^1.3.6"
semver: "npm:^7.7.3"
type-fest: "npm:^4.41.0"
yargs-parser: "npm:^21.1.1"
peerDependencies:
"@babel/core": ">=7.0.0-beta.0 <8"
"@jest/transform": ^29.0.0 || ^30.0.0
"@jest/types": ^29.0.0 || ^30.0.0
babel-jest: ^29.0.0 || ^30.0.0
jest: ^29.0.0 || ^30.0.0
jest-util: ^29.0.0 || ^30.0.0
typescript: ">=4.3 <6"
peerDependenciesMeta:
"@babel/core":
optional: true
"@jest/transform":
optional: true
"@jest/types":
optional: true
babel-jest:
optional: true
esbuild:
optional: true
jest-util:
optional: true
bin:
ts-jest: cli.js
checksum: 10/e0ff9e13f684166d5331808b288043b8054f49a1c2970480a92ba3caec8d0ff20edd092f2a4e7a3ad8fcb9ba4d674bee10ec7ee75046d8066bbe43a7d16cf72e
languageName: node
linkType: hard

"ts-morph@npm:^25.0.0":
version: 25.0.1
resolution: "ts-morph@npm:25.0.1"
Expand Down Expand Up @@ -14853,13 +14797,6 @@ __metadata:
languageName: node
linkType: hard

"type-fest@npm:^4.41.0":
version: 4.41.0
resolution: "type-fest@npm:4.41.0"
checksum: 10/617ace794ac0893c2986912d28b3065ad1afb484cad59297835a0807dc63286c39e8675d65f7de08fafa339afcb8fe06a36e9a188b9857756ae1e92ee8bda212
languageName: node
linkType: hard

"type-is@npm:~1.6.18":
version: 1.6.18
resolution: "type-is@npm:1.6.18"
Expand Down