reactter 3.0.0-dev.2 reactter: ^3.0.0-dev.2 copied to clipboard
Reactter is a light, powerful and reactive state management.
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.
You can refer to the following table to help you decide which package to use:
Project type | Packages |
---|---|
Dart only | |
Flutter |
Once you know what package you want to install, proceed to add the package on your project:
-
With command:
dart pub add reactter
For flutter:
flutter pub add flutter_reactter
-
Or add a line like this into your
pubspec.yaml
file:dependencies: reactter: #add version here
and then run
dart pub get
.For flutter:
dependencies: flutter_reactter: #add version here
and then run
flutter pub get
.
Now in your Dart code, you can use:
import 'package:reactter/reactter.dart';
for flutter:
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 extension of Reactter
that exposes some methods to helps 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 create 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>(id: 'uniqueId');
Using UseContext
hook #
UseContext
is a ReactterHook
that allows to get ReactterContext
's instance when 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 hook 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: It you have the instances, 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 context argument(this
) to put this hook on listen:
class AppContext extends ReactterContext {
late final count = UseState(0, this);
}
or add it on listenHooks
method which ReactterContext
exposes it:
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
exposes value
property that allows to read and writter 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 anotherObject
.NOTE: If you want to force notify, execute
update
method whichUseState
exposes it.
Using UseAsyncState
hook #
UseAsyncState
is a ReactterHook
with the same functionality as UseState
but provides a asyncValue
which it will be obtain when resolve
method is executed.
class AppContext extends ReactterContext {
late final asyncState = UseAsyncState<String?, Arguments>(null, _resolveState, this);
AppContext() {
_init();
}
Future<void> _init() async {
await asyncState.resolve(
Arguments(prop: true, prop2: "test"),
);
print("State resolved with: ${state.value}");
}
Future<String> _resolveState([Arguments args]) async {
return await api.getState(args.prop, args.prop2);
}
}
NOTE: If you want send argument to
asyncValue
method, need to defined a type arg which its send fromresolve
method. Like example shown above, which type argument send isArguments
class.
It also has when
method that returns a new value depending of it's state:
final valueComputed = asyncState.when<String>(
standby: (value) => "⚓️ Standby: $value",
loading: (value) => "⏳ Loading...",
done: (value) => "✅ Resolved: $value",
error: (error) => "❌ Error: $error",
);
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.
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(secounds: 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(
() async {
await Future.delayed(const Duration(seconds:s 1));
doCount();
},
[count],
UseEffect.dispatchEffect,
);
}
static void doCount() {
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: Don'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
witch 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.read
: Gets theReactterContext
's instance from the closest ancestor ofReactterProvider
.
final readContext = context.read<ReadContext>();
context.readId<T>
: Gets theReactterContext
's instance withid
from the closest ancestor ofReactterProvider
final readIdContext = context.readId<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 helps 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 helps 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
witch 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 keeping adding features for Reactter
, those are some we have in mind order by priority:
- Async context.
- Structure proposal for large projects.
- Do benchmarks.
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.
- Write articles or make videos teaching how to use Reactter.
Any idea is welcome!