Side Effects

Although side effects may seem confusing at first, they are the reason ReArch is so unique. Whenever some feature is not available in the core ReArch implementation, chances are you just need to create a side effect to add it!

Side effects in the Dart/Flutter implementation are heavily inspired by React hooks, but share some key differences. In the Rust implementation, side effects are largely a new paradigm, and may take some adjustment.

The Theory

This section delves a bit deeper into theory than other parts of the documentation; feel free to skip it and go straight to the examples if you wish.

In ReArch, side effects are modeled as:

  • Some private mutable data (only accessible to the side effect itself)
  • A way to mutate that private data outside of the build (which in turn triggers rebuilds)

Then, side effects expose an api that wrap around their private data and any external mutation(s).

For a concrete example, let's examine the state side effect. The state side effect's api is the current state and a function that sets that current state. Internally, the private mutable data is just the current state itself, and the rebuilding mutation exposed is just a function that sets the private data (the current state) to a given new state.

Although it may not seem like it, you can model any side effect using this same exact methodology.

Further, while capsules form a data flow/dependency graph, side effects form a tree. This enables side effects to have their own private side effect(s), enabling easy composition.

An Example

In Dart, do not conditionally call use.fooBar()! In Rust, do not conditionally call register(effect())! Doing so is considered an error; never wrap use.fooBar()/register(effect()) in an if statement. ReArch will likely throw a fit as soon as you mess this up, but it can't catch 100% of cases.

Here is a quick example showing how to use a few side effects. See the side effect library and api documentation for more information.

Using side effects
// A toy capsule that uses a few side effects
(Something, void Function(int)) usesSideEffects(CapsuleHandle use) {
  // First, let's create some mutable state
  final (state, setState) = use.state(1234);

  // Next, let's create an instance of some object using this state
  final somethingMadeWithState = use.memo(() => Something(state), [state]);

  // Finally, let's register this object to be disposed whenever a new one is created
  use.effect(() => somethingMadeWithState.dispose, [somethingMadeWithState]);

  // ...and let's return that object along with a way to set the state
  return (somethingMadeWithState, setState);
};

Widget Side Effects (Flutter Only)

In Flutter, in addition to the capsule-only SideEffect, there is also a WidgetSideEffect. WidgetSideEffects pack in some widget-specific functionality, like listening to widget lifecycle events and exposing a BuildContext.

When possible, try to write side effects as regular SideEffects (to enable reusability amongst capsules and widgets), although it is appropriate to make WidgetSideEffects:

  • When you need some widget-specific functionality
  • When it only would ever make sense to use the side effect from a widget (say, for an AnimationController).

See the builtin WidgetSideEffects.

Transactions

Multiple side effects can be updated in one batch rebuild via side effect transactions. This can be helpful when you wish to update unrelated data that needs to be updated together. Often, this is not needed. However, it can be helpful in some select situations (such as a "reset" action capsule that resets the state of several capsules at once).

Do not use transactions to update related or derived states! For example, do not make a capsule for a list of items paired with another capsule that stores the state of a set of the current "favorite" items. Doing so distributes one state across multiple sources, which is an anti-pattern in general. Instead, you should have a List<(Item, FavoritedStatus)> or similar that uses data composition. Then, you can put your derived states in other capsules (such as a list of only favorited items).

Only one side effect transaction may run at a time, but transactions may be nested such that you can run multiple other smaller transactions within the context of one overarching transaction. For example, two different capsules can update a few other capsules in their own transactions, and then another capsule can come along and aggregate both of those capsules in its own transaction! This allows each capsule to focus solely on the data it operates on (and nothing more), rather than worrying about where/how it is used.

If you are familiar with them, reentrant mutexes share some similarities to nested transactions; in fact, transactions are on top of a reentrant mutex in ReArch for Rust!

Without further ado, here is an example properly using transactions:

Using side effect transactions
(
  int, {
  void Function() increment,
  void Function() reset,
}) fooCapsule(CapsuleHandle use) {
  final (state, setState) = use.state(0);
  return (
    state,
    increment: () => setState(state + 1),
    reset: () => setState(0)
  );
}

(
  int, {
  void Function() increment,
  void Function() reset,
}) barCapsule(CapsuleHandle use) {
  final (state, setState) = use.state(0);
  return (
    state,
    increment: () => setState(state + 1),
    reset: () => setState(0)
  );
}

void Function() resetAction(CapsuleHandle use) {
  final resets = [use(fooCapsule).reset, use(barCapsule).reset];
  final runTxn = use.transactionRunner();
  return () => runTxn(() {
        for (final reset in resets) {
          reset();
        }
      });
}

Side Effect Library

See the side effect library on the left sidebar for documentation on some of the builtin side effects!

Creating Custom Side Effects

Help is wanted for this section!

In the meantime, take a peak at the source code for the builtin side effects, as a lot of them use composition under the hood and are a good starting point.