A light, powerful and reactive state management.
Features
- ⚡️ Build for speed.
- 📏 Reduce boilerplate code significantly.
- 📝 Improve code readability.
- 🪃 Unidirectional data flow.
- ♻️ Reuse state using custom hooks.
- 🪄 No configuration necessary.
- 🎮 Total control to re-render widget tree.
- 💙 Flutter or Dart only, you can use in any Dart project.
Contents
Quickstart
Before anything, you need to be aware that Reactter is distributed on two packages, with slightly different usage.
The package of Reactter that you will want to install depends on the project type you are making.
Select one of the following options to know how to install it:
Dart only
Add the package on your project.
-
Using command:
dart pub add reactter
-
Or put directly into
pubspec.yaml
file:dependencies: reactter: #add version here
and run
dart pub get
.
Now in your Dart code, you can use:
import 'package:reactter/reactter.dart';
Flutter
Add the package on your project.
-
Using command:
flutter pub add flutter_reactter
-
Or put directly into
pubspec.yaml
file:dependencies: flutter_reactter: #add version here
and run
flutter pub get
.
Now in your Dart code, you can use:
import 'package:flutter_reactter/flutter_reactter.dart';
Usage
Create a ReactterContext
ReactterContext
is a abstract class that allows to manages ReactterHook
and provides life-cycle events.
You can use it's functionalities, creating a class that extends it:
class AppContext extends ReactterContext {}
RECOMMENDED: Name class with
Context
suffix, for easy locatily.
Lifecycle of ReactterContext
ReactterContext
has the following Lifecycle events:
-
Lifecycle.registered
: Event when the instance has registered byReactterInstanceManager
. -
Lifecycle.unregistered
: Event when the instance has unregistered byReactterInstanceManager
. -
Lifecycle.inicialized
: Event when the instance has inicialized byReactterInstanceManager
. -
Lifecycle.willMount
: Event when the instance will be mount in the widget tree (it use withflutter_reactter
only). -
Lifecycle.didMount
: Event when the instance did be mount in the widget tree (it use withflutter_reactter
only). -
Lifecycle.willUpdate
: Event when any instance's hooks will be update. Event param is aReactterHook
. -
Lifecycle.didUpdate
: Event when any instance's hooks did be update. Event param is aReactterHook
. -
Lifecycle.willUnmount
: Event when the instance will be unmount in the widget tree(it use withflutter_reactter
only). -
Lifecycle.destroyed
: Event when the instance did be destroyed byReactterInstanceManager
.
You can put it on listen, using UseEvent
, for example:
UseEvent<AppContext>().on<ReactterHook>(
Lifecycle.didUpdate,
(inst, hook) => print("Instance: $inst, hook: $hook"),
);
Manage instance with ReactterInstanceManage
ReactterInstanceManager
is a instance of Reactter
that exposes some methods to manages instance. These are some methods:
Reactter.register
: Registers a builder
function to allows to create the instance using Reactter.get
.
Reactter.register(builder: () => AppContext());
Reactter.register(id: "uniqueId", builder: () => AppContext());
Reactter.unregister
: Removes the builder
function to avoid create the instance.
Reactter.unregister<AppContext>();
Reactter.unregister<AppContext>("uniqueId");
Reactter.get
: Gets the previously instance created or creates a new instance from the build
registered using reactter.register
.
final appContext = Reactter.get<AppContext>();
final appContextWithId = Reactter.get<AppContext>(id: 'uniqueId');
Reactter.create
: Registers, creates and gets the instance directly.
final appContext = Reactter.create(build: () => AppContext());
final appContextWithId = Reactter.create(id: 'uniqueId', build: () => AppContext());
Reactter.delete
: Deletes the instance but still keep the build
function.
Reactter.delete<AppContext>();
Reactter.delete<AppContext>('uniqueId');
NOTE: The registered instances have a global scope. This means that you can access them anywhere in the project just by using
Reactter.get
or throughUseContext
.
Using UseContext
hook
UseContext
is a ReactterHook
that allows to get ReactterContext
's instance when it's ready.
class AppContext extends ReactterContext {
late final otherContextHook = UseContext<OtherContext>(context: this);
// final otherContextHookWithId = UseContext<OtherContext>(id: "uniqueId", context: this);
late otherContext = otherContext.instance;
AppContext() {
UseEffect(() {
otherContext = otherContextHook.instance;
}, [otherContextHook]);
}
}
NOTE: If you're not sure that you got the instance from the beginning, you need to use the
UseEffect
as shown in the example above.NOTE: The context that you need to get, must be created by
ReactterInstanceManager
.
Using UseEvent
hook
UseEvent
is a ReactterHook
that manages events.
You can listen to event using on
method:
enum Events { SomeEvent };
void _onSomeEvent(inst, param) {
print("$inst's Events.SomeEvent emitted with param: $param.");
}
UseEvent<AppContext>().on(Events.SomeEvent, _onSomeEvent);
Use off
method to stop listening event:
UseEvent<AppContext>().off(Events.SomeEvent, _onSomeEvent);
If you want to listen event only once, use one
method:
UseEvent<AppContext>().one(Events.SomeEvent, _onSomeEvent);
And use emit
method to trigger event:
UseEvent<AppContext>().emit(Events.SomeEvent, 'Parameter');
IMPORTANT: Don't forget to remove event using
off
or usingdispose
to remove all instance's events. Failure to do so could increase memory usage or have unexpected behaviors, such as events in permanent listening.RECOMMENDED: If you have the instance, use directly with
UseEvent.withInstance(<instance>)
.
Using UseState
hook
UseState
is a ReactterHook
that manages a state.
You can add it on any part of class, with the context argument(this
) to put this hook on listen:
class AppContext extends ReactterContext {
late final count = UseState(0, this);
}
or add it into listenHooks
method which is exposed by ReactterContext
:
class AppContext extends ReactterContext {
final count = UseState(0);
AppContext() {
listenHooks([count]);
}
}
NOTE: If you don't add context argument or use
listenHook
, theReactterContext
won't be able to react to hook's changes.
UseState
has value
property that allows to read and write its state:
class AppContext extends ReactterContext {
late final count = UseState(0, this);
AppContext() {
print("Prev state: ${count.value}");
count.value = 10;
print("Current state: ${count.value}");
}
}
NOTE:
UseState
notifies that its state has changed when the previous state is different from the current state. If its state is aObject
, not detect internal changes, only when states is setted anotherObject
.
If you want to force notify, execute update
method, which is exposed by UseState
.
class Todo {
String name;
Todo(this.name);
}
class AppContext extends ReactterContext {
final todoState = UseState(Todo('Do this'), this);
AppContext() {
todoState.update(() {
todoState.value.name = 'Do this other';
});
}
}
Using UseAsyncState
hook
UseAsyncState
is a ReactterHook
with the same functionality as UseState
but provides a asyncValue
method, which will be obtained when resolve
method is executed.
class TranslateArgs {
final String to;
final String from;
final String text;
TranslateArgs({ this.to, this.from, this.text });
}
class AppContext extends ReactterContext {
late final translateState = UseAsyncStates<String, TranslateArgs>(
'Hello world',
translate,
this,
);
AppContext() {
_init();
}
Future<void> _init() async {
await translateState.resolve(
TranslateArgs({
to: 'ES',
from: 'EN',
text: translateState.value,
}),
);
print("'Hello world' translated to Spanish: '${translateState.value}'");
}
Future<String> translate([TranslateArgs args]) async {
return await api.translate(args);
}
}
NOTE: If you want to send argument to
asyncValue
method, need to define a type argument which be send throughresolve
method. Like the example shown above, the argument type send isTranslateArgs
class.
It also has when
method that returns a new value depending on it's state:
final valueComputed = asyncState.when<String>(
standby: (value) => "⚓️ Standby: $value",
loading: (value) => "⏳ Loading...",
done: (value) => "✅ Resolved: $value",
error: (error) => "❌ Error: $error",
);
Using UseReducer
hook
UseReducer
is a ReactterHook
that manages state using reducer method. An alternative to UseState
.
RECOMMENDED:
UseReducer
is usually preferable toUseState
when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one.
UseReducer
accepts three arguments:
UseReducer(<reducer>, <initialState>, <context>);
- The
reducer
method contains your custom state logic that calculates the new state using current state, and actions. - The
initialState
is a unique value of any type with which you initialize the state. - The
context
represents any instance of theReactterContext
which is notified of any change in state.
UseReducer
exposes a dispatch
method that allows to invoke the reducer
method sending a ReactterAction
.
The current state can be access through value
property.
Here's the counter example:
class Store {
final int count;
Store({this.count = 0});
}
Store _reducer(Store state, ReactterAction<String, int?> action) {
switch (action.type) {
case 'INCREMENT':
return Store(count: state.count + (action.payload ?? 1));
case 'DECREMENT':
return Store(count: state.count + (action.payload ?? 1));
default:
throw UnimplementedError();
}
}
class AppContext extends ReactterContext {
late final state = UseReducer(_reducer, Store(count: 0), this);
AppContext() {
print("count: ${state.value.count}"); // count: 0;
state.dispatch(ReactterAction(type: 'INCREMENT', payload: 2));
print("count: ${state.value.count}"); // count: 2;
state.dispatch(ReactterAction(type: 'DECREMENT'));
print("count: ${state.value.count}"); // count: 1;
}
}
Also, you can create the actions as a callable class, extending from ReactterActionCallable
and use them like this:
class Store {
final int count;
Store({this.count = 0});
}
class IncrementAction extends ReactterActionCallable<Store, int> {
IncrementAction({int quantity = 1}) : super(type: 'INCREEMNT', payload: quantity);
@override
Store call(Store state) => Store(count: state.count + payload);
}
class DecrementAction extends ReactterActionCallable<Store, int> {
DecrementAction({int quantity = 1}) : super(type: 'DECREMENT', payload: quantity);
@override
Store call(Store state) => Store(count: state.count - payload);
}
Store _reducer(Store state, ReactterAction action) =>
action is ReactterActionCallable ? action(state) : UnimplementedError();
class AppContext extends ReactterContext {
late final state = UseReducer(_reducer , Store(count: 0), this);
AppContext() {
print("count: ${state.value.count}"); // count: 0;
state.dispatch(IncrementAction(quantity: 2));
print("count: ${state.value.count}"); // count: 2;
state.dispatch(DecrementAction());
print("count: ${state.value.count}"); // count: 1;
}
}
Using UseEffect
hook
UseEffect
is a ReactterHook
that manages side-effect.
You can add it on constructor of class:
class AppContext extends ReactterContext {
late final count = UseState(0, this);
AppContext() {
UseEffect((){
// Execute by count state changed or 'didMount' event
print("Count: ${count.value}");
Future.delayed(
const Duration(seconds: 1),
() => count.value += 1,
);
return () {
// Cleanup - Execute Before count state changed or 'willUnmount' event
print("Cleanup executed");
};
}, [count], this);
}
}
If you want to execute a UseEffect
immediately, use UseEffect.dispatchEffect
instead of the context
argument:
UseEffect(
() => print("Excute immediately or by hook's changes"),
[someHook],
UseEffect.dispatchEffect
);
NOTE: If you don't add
context
argument toUseEffect
, thecallback
don't execute on lifecycledidMount
, and thecleanup
don't execute on lifecyclewillUnmount
(theses lifecycle events are used withflutter_reactter
only).
Create a ReactterHook
ReactterHook
is a abstract class that allows to create a Custom Hook.
There are several advantages to using Custom Hooks:
- Reusability: you can use the same hook again and again, without the need to write it twice.
- Clean Code: extracting part of context logic into a hook will provide a cleaner codebase.
- Maintainability: easier to maintain. if we need to change the logic of the hook, you only need to change it once.
Here's the counter example:
class UseCount extends ReactterHook {
int _count = 0;
int get value => _count;
UseCount(int initial, [ReactterContext? context])
: _count = initial,
super(context);
void increment() => update(() => _count += 1);
void decrement() => update(() => _count -= 1);
}
RECOMMENDED: Name class with
Use
preffix, for easy locatily.NOTE:
ReactterHook
providesupdate
method which notify tocontext
that has changed.
and use it like that:
class AppContext extends ReactterContext {
late final count = UseCount(0, this);
AppContext() {
UseEffect(() {
Future.delayed(
const Duration(seconds: 1),
count.increment,
);
print("Count: ${count.value}");
}, [count], this);
}
}
Global state
The reactter's hooks can be defined as static to access its as global way:
class Global {
static final flag = UseState(false);
static final count = UseCount(0);
// Create a class factory to run it as singleton way.
// This way, the initial logic can be executed.
static final Global _inst = Global._init();
factory Global() => _inst;
Global._init() {
UseEffect(() {
Future.delayed(const Duration(seconds: 1), changeCount);
}, [count], UseEffect.dispatchEffect);
}
static void changeCount() {
if (count.value <= 0) {
flag.value = true;
}
if (count.value >= 10) {
flag.value = false;
}
flag.value ? count.increment() : count.decrement();
}
}
// It's need to create the instance it to be able
// to execute Global._init(This executes only once).
final global = Global();
This is a example that how you could use it:
class AppContext extends ReactterContext {
late final isOdd = UseState(false, this);
AppContext() {
UseEffect((){
isOdd.value = Global.count.value % 2 != 0;
}, [Global.count], this);
}
}
NOTE: If you want to execute some logic when initialize the global class, you need to use the class factory and then instance it to run as singleton way.
Usage with flutter_reactter
Wrap with ReactterProvider
ReactterProvider
is a wrapper StatelessWidget
that provides a
ReactterContext
's instance to widget tree that can be access through the BuildContext
.
ReactterProvider(
() => AppContext(),
builder: (context, child) {
final appContext = context.watch<AppContext>();
return Text("count: ${appContext.count.value}");
},
)
If you want to create a different ReactterContext
's instance, use id
parameter.
ReactterProvider(
() => AppContext(),
id: "uniqueId",
builder: (context, child) {
final appContext = context.watchId<AppContext>("uniqueId");
return Text("count: ${appContext.count.value}");
},
)
IMPORTANT: Dont's use
ReactterContext
with constructor parameters to prevent conflicts. Instead useonInit
method to access its instance and put the data you need.NOTE:
ReactteProvider
is a "scoped". So it contains aReactterScope
which thebuilder
callback will be rebuild, when theReactterContext
changes. For this to happen, theReactterContext
should put it on listens forBuildContext
'swatch
ers.
Access to ReactterContext
Reactter provides additional methods to BuildContext
to access your ReactterContext
. These are following:
context.watch
: Gets theReactterContext
's instance from the closest ancestor ofReactterProvider
and watch allReactterHook
orReactterHook
defined in first paramater.
final watchContext = context.watch<WatchContext>();
final watchHooksContext = context.watch<WatchHooksContext>(
(ctx) => [ctx.stateA, ctx.stateB],
);
context.watchId
: Gets theReactterContext
's instance withid
from the closest ancestor ofReactterProvider
and watch allReactterHook
orReactterHook
defined in second paramater.
final watchIdContext = context.watchId<WatchIdContext>('id');
final watchHooksIdContext = context.watchId<WatchHooksIdContext>(
'id',
(ctx) => [ctx.stateA, ctx.stateB],
);
context.use
: Gets theReactterContext
's instance with/withoutid
from the closest ancestor ofReactterProvider
.
final readContext = context.use<ReadContext>();
final readIdContext = context.use<ReadIdContext>('id');
NOTE: These methods mentioned above uses
ReactterProvider.contextOf
NOTE:
context.watch
andcontext.watchId
watch all or some of the specifiedReactterHook
dependencies and when it will change, re-render widgets in the scope ofReactterProviders
,ReactterBuilder
orReactterScope
.
Control re-render with ReactterScope
ReactterScope
is a wrapeer StatelessWidget
that to to control re-rendered of widget tree.
ReactterScope<AppContext>(
builder: (context, child) {
final appContext = context.watch<AppContext>();
return Text("Count: ${appContext.count.value}");
},
)
NOTE: The
builder
callback will be rebuild, when theReactterContext
changes. For this to happen, theReactterContext
should put it on listens forBuildContext
'swatch
ers.
Control re-render with ReactterBuilder
ReactterBuilder
is a wrapper StatelessWidget
that to to get the ReactterContext
's instance from the closest ancestor of ReactterProvider
and exposes it through the first parameter of builder
callback.
ReactterBuilder<AppContext>(
listenAllHooks: true,
builder: (appContext, context, child) {
return Text("Count: ${appContext.count.value}");
},
)
NOTE:
ReactterBuilder
is read-only by default(listenAllHooks: false
), this means it only renders once. Instead uselistenAllHooks
astrue
or uselistenHooks
with theReactterHook
s specific and then thebuilder
callback will be rebuild with everyReactterContext
'sReactterHook
changes.NOTE:
ReactterBuilder
is a "scoped". So it contains aReactterScope
which thebuilder
callback will be rebuild, when theReactterContext
changes. For this to happen, theReactterContext
should put it on listens forBuildContext
'swatch
ers.
Multiple ReactterProvider
with ReactterProviders
ReactterProviders
is a wrapper StatelessWidget
that allows to use multiple ReactterProvider
as nested way.
ReactterProviders(
[
ReactterProvider(() => AppContext()),
ReactterProvider(
() => ConfigContext(),
id: 'App',
onInit: (appConfigContext) {
appConfigContext.config.value = 'new state';
},
),
ReactterProvider(
() => ConfigContext(),
id: 'User'
),
],
builder: (context, child) {
final appContext = context.watch<AppContext>();
final appConfigContext = context.watchId<ConfigContext>('App');
final userConfigContext = context.watchId<ConfigContext>('User');
...
},
)
Create a ReactterComponent
ReactterComponent
is a abstract StatelessWidget
class that provides the functionality of ReactterProvider
with a ReactterContext
and exposes it through render
method.
class CounterComponent extends ReactterComponent<AppContext> {
const CounterComponent({Key? key}) : super(key: key);
@override
get builder => () => AppContext();
@override
get id => 'uniqueId';
@override
listenHooks(appContext) => [appContext.stateA];
@override
Widget render(appContext, context) {
return Text("StateA: ${appContext.stateA.value}");
}
}
Resources
Roadmap
We want to keeping adding features for Reactter
, those are some we have in mind order by priority:
- Widget to control re-render using only hooks
- Async context.
- Do benchmarks and improve performance.
Contribute
If you want to contribute don't hesitate to create an issue or pull-request in Reactter repository.
You can:
- Provide new features.
- Report bugs.
- Report situations difficult to implement.
- Report an unclear error.
- Report unclear documentation.
- Add a new custom hook.
- Add a new widget.
- Add examples.
- Translate documentation.
- Write articles or make videos teaching how to use Reactter.
Any idea is welcome!