@blac/react
A powerful React integration for the Blac state management library, providing seamless integration between React components and Blac's reactive state management system.
Features
- 🔄 Automatic re-rendering when relevant state changes
- 🎯 Fine-grained dependency tracking
- 🔍 Property access tracking for optimized updates
- 🎨 TypeScript support with full type inference
- ⚡️ Efficient state management with minimal boilerplate
- 🔄 Support for isolated and shared bloc instances
- 🎯 Custom dependency selectors with access to state, previous state, and instance
- 🚀 Optimized re-rendering with intelligent snapshot comparison
Important: Arrow Functions Required
All methods in Bloc or Cubit classes must use arrow function syntax (method = () => {}) instead of the traditional method syntax (method() {}). This is because arrow functions automatically bind this to the class instance. Without this binding, methods called from React components would lose their context and could not access instance properties like this.state or this.emit().
class CounterBloc extends Cubit<CounterState> {
increment = () => {
this.emit({ ...this.state, count: this.state.count + 1 });
}
decrement = () => {
this.emit({ ...this.state, count: this.state.count - 1 });
}
}
class CounterBloc extends Cubit<CounterState> {
increment() {
this.emit({ ...this.state, count: this.state.count + 1 });
}
}
Installation
npm install @blac/react
yarn add @blac/react
pnpm add @blac/react
Quick Start
import { useBloc } from '@blac/react';
import { CounterBloc } from './CounterBloc';
function Counter() {
const [state, counterBloc] = useBloc(CounterBloc);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => counterBloc.increment()}>Increment</button>
</div>
);
}
Usage
Basic Usage
The useBloc hook provides a simple way to connect your React components to Blac's state management system:
const [state, bloc] = useBloc(YourBloc);
Advanced Configuration
The hook accepts configuration options for more control:
const [state, bloc] = useBloc(YourBloc, {
id: 'custom-id',
props: { },
onMount: (bloc) => { },
selector: (currentState, previousState, instance) => [],
});
Automatic Dependency Tracking
The hook automatically tracks which state properties and bloc instance properties are accessed in your component and only triggers re-renders when those specific values change:
function UserProfile() {
const [state, userBloc] = useBloc(UserBloc);
return <h1>{state.name}</h1>;
}
This also works for getters and computed properties on the Bloc or Cubit class:
function UserProfile() {
const [state, userBloc] = useBloc(UserBloc);
return <h1>{userBloc.fullName}</h1>;
}
How It Works
The dependency tracking system uses JavaScript Proxies to monitor property access during component renders:
- State Properties: When you access
state.propertyName, it's automatically tracked
- Instance Properties: When you access
bloc.computedValue, it's automatically tracked
- Intelligent Comparison: The system separately tracks state dependencies and instance dependencies to handle edge cases where properties are dynamically added/removed
- Optimized Updates: Components only re-render when tracked dependencies actually change their values
Configuring Proxy Tracking
By default, BlaC uses proxy-based dependency tracking for optimal performance. You can disable this globally if needed:
import { Blac } from '@blac/core';
Blac.setConfig({ proxyDependencyTracking: false });
When proxy tracking is disabled:
- Components re-render on any state change (similar to traditional state management)
- Manual dependencies via the
selector option still work as expected
- Useful for debugging or when proxy behavior causes issues
For more configuration options, see the @blac/core documentation.
Custom Dependency Selector
For more control over when your component re-renders, you can provide a custom dependency selector. The selector function receives the current state, previous state, and bloc instance, and should return an array of values to track:
const [state, bloc] = useBloc(YourBloc, {
selector: (currentState, previousState, instance) => [
currentState.specificField,
currentState.anotherField,
instance.computedValue
]
});
The component will only re-render when any of the values in the returned array change (using Object.is comparison, similar to React's useEffect dependency array).
Examples of Custom Selectors
Track only specific state properties:
const [state, userBloc] = useBloc(UserBloc, {
selector: (currentState) => [
currentState.name,
currentState.email
]
});
Track computed values:
const [state, shoppingCartBloc] = useBloc(ShoppingCartBloc, {
selector: (currentState, previousState, instance) => [
instance.totalPrice,
currentState.items.length
]
});
Compare with previous state:
const [state, chatBloc] = useBloc(ChatBloc, {
selector: (currentState, previousState) => [
currentState.messages.length > (previousState?.messages.length || 0) ? 'new-message' : 'no-change'
]
});
API Reference
useBloc Hook
function useBloc<B extends BlocConstructor<BlocGeneric>>(
bloc: B,
options?: BlocHookOptions<InstanceType<B>>
): [BlocState<InstanceType<B>>, InstanceType<B>]
Options
id?: string - Custom identifier for the bloc instance
props?: InferPropsFromGeneric<B> - Props to pass to the bloc
onMount?: (bloc: B) => void - Callback function invoked when the react component (the consumer) is connected to the bloc instance
selector?: (currentState: BlocState<InstanceType<B>>, previousState: BlocState<InstanceType<B>> | undefined, instance: InstanceType<B>) => unknown[] - Function to select dependencies for re-renders
Best Practices
-
Use Isolated Blocs: When you need component-specific state, use isolated blocs:
class MyIsolatedBloc extends BlocBase {
static isolated = true;
}
-
Use Custom Identifiers: When you need multiple independent instances of the same Bloc type, use custom identifiers to manage different state contexts:
function ChatRoom({ roomId }: { roomId: string }) {
const [state, chatBloc] = useBloc(ChatBloc, {
id: `chat-${roomId}`,
props: { roomId }
});
return (
<div>
<h2>Room: {roomId}</h2>
{state.messages.map(msg => (
<Message key={msg.id} message={msg} />
))}
</div>
);
}
function ChatApp() {
return (
<div>
<ChatRoom roomId="general" />
<ChatRoom roomId="support" />
</div>
);
}
-
Choose the Right Dependency Strategy:
- Use automatic tracking (default) for most cases - it's efficient and requires no setup
- Use custom selectors when you need complex logic, computed comparisons, or want to ignore certain property changes
- Avoid custom selectors for simple property access - automatic tracking is more efficient
-
Type Safety: Take advantage of TypeScript's type inference for better development experience and catch errors early.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
MIT