question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Alternatives Async/side effect models

See original GitHub issue

it’s been some time since I’m thinking on Async/Side effects models in Elm architecture. I’d really like to write some article on this to complete the first one. I know it was already discussed in #13 but i’d like to present some other alternatives here for discussion. i´ll motivate the need for alternative models and will illustrate with the standard Counter example. Sorry for the long post.

Currently the standard Elm solution works by turning the signature of update from this

update : (state, action) -> state

to this

update : (state, action) -> (state, [Future Action])

// using Reactive streams
update : (state, action) -> (state, Stream Action)

// using callback with Node CPS style
update : (state, action, dispatch -> () ) -> state

Whatever solution we choose, it allows us to express side effects and fire asynchronous actions. However, there is a thing i dislike here : in the first effect-less version we had a nice pure function (state, action) -> state. It’s predictable and can be tested with ease. In contrast, it’s not that easy in the effect-full version : we now have to mock the function environment to test the effect-full reactions.

IMO it’d be preferable to keep the update pure and clean of any side effect. Its only purpose should be : calculate a new state giving an existing one and a given action.

So i present here 2 alternatives side effect models for discussion :

1- The first model is taken directly from redux (which was inspired by Elm). Like in Elm architecture, redux has pure functions called reducers with the same signature as update. A pretty standard way to fire asynchronous actions in redux is through thunks : i.e. callback function of the form dispatch -> ().

I’ll illustrate with the Counter example and an asynchronous increment action

// Synchronous : () -> Action
function increment() {
  return { type: 'increment' }
}

// Asynchronous : () -> ( dispatch -> () )
function incrementAsync() {
  return dispatch => {
    setTimeout( () => dispatch( increment() ),  1000)
  }
}

const view = ({state, dispatch}) =>
    <div>
      <button on-click={[dispatch, increment()]}>+</button>
      <div>{state}</div>
      <button on-click={[dispatch, incrementAsync()]}>+ (Async)</button>
    </div>;

const init = () => 0;

const update = (state, action) => type === 'increment' ? state + 1 : state;

The idea is : instead of firing a normal action, the component fires an action dispatcher (the thunk). Now our main dispatcher has to be enhanced to know about thunks

function dispatch(action) {
  if(typeof action === 'function')
    action(dispatch);
  else {
    state = Counter.update(action);
    updateUI();
  }
}

The main benefit of the above approach is to keep the update function clean of any effect (this is also a golden rule in redux : a reducer must not do any side effect like api calls…). Another benefit is that you can fire multiples actions from the thunk which is useful to manage a flow of multiple actions.

2- the second alternative was inspired by this interesting discussion (also in the redux site, yeah!), the idea is to separate Effect creation and execution in the same way we separate Action creation and update, the update signature will look quite the same as in the current Elm solution

update : (state, action) -> (state, Effect)

However, the Effect above is no longer a Future or a Promise or whatever, it’s a just a data object much like the actions that describes the intent. To run the actual effect we’ll provide the component with another method execute which will run the real side effect. So back to the Counter example


// an union type to describe "effect-full" value
const UpdateResult = Type({
  Pure: [T],
  WithEffects: [T, T]
});


const Action = Type({
  Increment      : [],
  IncrementLater : []
})

const Effect = Type({
  IncrementAsync : []
});

const view = ({state, dispatch}) =>
    <div>
      <button on-click={[dispatch, Action.Increment()]}>+</button>
      <div>{state}</div>
      <button on-click={[dispatch, Action.IncrementLater()]}>+ (Async)</button>
    </div>;

// pure and withEffects are 2 helper factories
const init = () => pure(0);

const update = (state, action) => Action.case({
  Increment       : () => pure(state + 1),
  IncrementLater  : () => withEffects(state, Effect.IncrementAsync())
}, action);

const execute = (state, effect, dispatch) => Effect.case({
  IncrementAsync: () => {
    setTimeout(() => dispatch(Action.Increment()), 1000)
  }
}, effect);

export default { view, init, update, Action, execute, Effect };

So the component have now 2 additional properties : Effect which like Action documents side effects carried by the component and execute, which like update, run the actual effect. This may seem more boilerplate but has the advantage of keeping the update function pure : we can now test its return value by just checking the returned state and eventually the side effect data. Another advantage may be that Effects are now documented explicitly in the component.

The main dispatcher will look something like

function updateStatePure(newState) {
  state = newState;
  updateUI();
}

export function dispatch(action) {
  const updateResult = App.update(state, action);
  UpdateResult.case({
    Pure: v => updateStatePure(v),
    WithEffects: (v, effect) => {
      updateStatePure(v);
      Counter.execute(effect, dispatch);
    }
  })
}

Here is an example of the above approach with nested components.

What do you think of those 2 models ? the redux solution seems more simple to me as it doesn’t add more boilerplate to the current model. The 2nd solution has the advantage to be more explicit and clearly documents the Effects carried by a component.

I’d be also nice to hear the opinion of @evancz or also @gaearon the creator of redux, knowing his Elm background

Issue Analytics

  • State:open
  • Created 8 years ago
  • Comments:37 (1 by maintainers)

github_iconTop GitHub Comments

3reactions
yelouaficommented, Nov 30, 2015

@tomkis1 I did it exactly for the reasons you mentioned in the blogpost; data is easier to test than thunks (no mock is necessary). Another benefit is that the logger now logs also triggered side effects, so we have full traceability of the events in our app.

Since you already tried this approach in production; can you elaborate on its disadvantages; did you find that the complications outweigh the benefits ?

1reaction
slorbercommented, Dec 22, 2015

@tomkis1 thanks I’m trying to do my best haha but I’m not even an expert in backend Sagas btw…

ContentScrolledNearBottom would also be my choice, but then something should still trigger the fetching of the next page. In this case the saga pattern seems nice to do it.

I don’t think the saga should necessarily be used everytime an effect has to be done. What I like about this pattern applied to the frontend is the ability to make the implicit explicit. I mean it is easier to understand that a page is loading because there is a “PageLoaded” event than because there is a “ScrolledNearBottom” event.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Episode #78: Effectful State Management: Asynchronous Effects
It's time to finish our architecture's story for side effects. We've described synchronous effects and unidirectional effects, ...
Read more >
Async Generators as an alternative to State Management
Unlike a state management approach, async generators tame asynchronicity leaving mutations harmless (if visible only in the generator's scope).
Read more >
Mutiny - Async for bare mortal - Quarkus
It is the primary model to write reactive applications with Quarkus. ... This is particularly convenient to execute asynchronous side effect actions.
Read more >
Advanced State and Side Effects in Jetpack Compose
Side effect APIs such as LaunchedEffect , rememberUpdatedState , DisposableEffect , produceState , and derivedStateOf . How to create coroutines ...
Read more >
Async/Await - Best Practices in Asynchronous Programming
In particular, it's usually a bad idea to block on async code by calling Task.Wait or Task.Result. This is an especially common problem...
Read more >

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found