Skip to content

Instantly share code, notes, and snippets.

@craigspaeth
Last active August 9, 2020 14:31
Show Gist options
  • Star 14 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save craigspaeth/7090e6b5fec11df29c62de9250c959c0 to your computer and use it in GitHub Desktop.
Save craigspaeth/7090e6b5fec11df29c62de9250c959c0 to your computer and use it in GitHub Desktop.
Baobab vs. Redux

Thoughts on Redux vs. Baobab

I'm going to write down my thoughts on these two libraries that I'm conflicted between.

Disclaimer: These are just my personal stream of consciousness notes and not meant to be a well thought through blog post/opinion piece. Both these libraries are fantastic and the authors deserve huge props/respect. If you find these musings helpful I'm glad, but I encourage you to take it with a grain of salt.

Intro

Redux is a simplified Flux, with a small footprint, and an architecture that encourages writing your business logic as pure functions. In Redux views call action creators, action creators kick off side effects and further actions, actions are caught by reducers which return an updated state blob (scoped by branches of the state tree).

At the end of the day Baobab has a similar concept—put all of your state in one giant immutable blob. Baobab however doesn't prescribe architecture on your app, it just provides a single immtutable + persistent tree object. A Baobab tree also has cursors to store references to branches of the state tree, and "monkies" to return computed "views" of your state data (e.g. what Reselect does for Redux). In Baobab views simply call functions that update the state tree causing re-renders.

I think there is value to both approaches, but I tend to favor conceptually easier with minimal concepts to grasp over implementations that add overhead in the name of code quality. While I do buy into the benefits of things like pure functions, I'm not convinced yet they're worth it as a widely enforced pattern. Interestingly Redux's attempt at being minimalistic actually leaves out some things that end up burdening the developer pretty quickly such as not wrapping state in an immutable data structure and not having a way of doing computed values from state out of the box.

Example

To demo with code the overhead Redux has I'll take an example of doing an autocomplete UI where upon keyup of an input we make an api request to search for members in our database and update the members state to render the newly filtered members. (This is a use case roughly based on our Team Navigator search).

Because I'm just jotting these thoughts down for myself—not trying to prove a point just yet—I'm going to first go fully idiomatic React/Redux/JSX vs. Baobab/React-DOM-API/Vanilla JS, then I'll try to reduce the Redux example to a hypothetically minimal implementation.

Redux

View

export default class Search extends React.Component {

  render({ dispatch, members }) {
    return <div>
      <ul>
        {members.map((member) => member.name)}
      </ul>
      <input
        onKeyUp={(e) => dispatch(searchMembers(e.target.value))}
      />
    </div>
  }
}

Search.propTypes = {
  members: PropTypes.array.isRequired
}

Actions

export const SEARCH_MEMBERS = 'SEARCH_MEMBERS'
export const FOUND_MEMBERS = 'FOUND_MEMBERS'

Action Creators

export const searchMembers = (term) => {
  return { type: SEARCH_MEMBERS, term }
}

export const foundMembers = (members) => {
  return { type: FOUND_MEMBERS, members }
}

export const searchMembers = (term) => {
  return async (dispatch) => {
    dispatch(searchMembers(term))
    const { members } = await api.query(`{ members(term: ${term}) { name } }`)
    dispatch(foundMembers(members))
  }
}

Reducers

export const searchingMembers = (state, action) => {
  switch action.type {
    case 'SEARCH_MEMBERS':
      return true
    case 'FOUND_MEMBERS':
      return false
    default:
      return false
  }
}

export const members = (state, action) => {
  switch action.type {
    case 'FOUND_MEMBERS':
      return action.members
    default:
      return []
  }
}

😵 I can't believe this is the favored Redux architecture. Now for a minimal Baobab implementation...

Baobab

View

const searchMembers = async (state, term) => {
  state.set({ searchingMembers: true })
  const { members } = await api.query(`{ members(term: ${term}) { name } }`)
  state.set({ members, searchingMembers: false })
}

export default (_, { state }) =>
  div(
    input({
      onKeyUp: (e) => searchMembers(state, e.target.value)
    }),
    ul(state.get('members').map((member) =>
      li(member.name)))

And that extra Redux code multiplies linearly with every new interaction with async/side effects the app has (which tends to be at-least half of them). It seems like Redux trades one simple impure function for a bunch of pure functions and often at-least one impure function.

That said, it's even pretty easy to write the above Baobab example with pure functions by simply breaking apart the three lines of searchMembers into three functions where the first and last return state.set(...)—so I don't see why this is something that needs to be forced on to the programmer with so much boilerplate.

Simplifying Redux

Given Redux's popularity though, it might be worth using it anyways and see if we can manage to reduce it down to the simplest parts. This would involve eschewing a lot of the encouraged design patterns and attempting to expose the Action/Reducer paradigm in the most direct way possible. Potentially that could look like...

View

export default (_, { state, action }) =>
  div(
    input({ onKeyUp: action('searchMembers') }),
    ul(state.members.map((member) =>
      li(member.name)))

Controller

export const searchMembers = (state, action, e) => {
  api.query(`{ members(term: ${term}) { name } }`)
    .then(action('foundMembers'))
  return state.set({ searchingMembers: true })
}

export const foundMembers = (state, action, members) => {
  return state.set({ members })
}

Essentially this would implement a middle layer between reducers and actions that pass an immutable map wrapping Redux's state and the arguments passed into the action thunk. The controller functions return the entire new state object directly though the main Redux reducer.

This is still unideal because Redux forces us to handle state changes in a synchronous (state, action) => newState manner, so we can't express that logic linearlly like we do with Baobab—we have to send another action after the async part. This is where the case for a function tree comes. While a function tree does manage to do a better job with readability, co-location of logic, and maintaining the impure/pure separation—it does add overhead/boilerplate of its own by adding a new way to do control flow on top of understanding the action + reducer + function-tree context.

Pure Functions?

So all of this overhead in Redux is so we can acheive pure functions. Clearly pure functions are cool for many reasons but is it really worth it when in most cases you have side effects to deal with and you either obscure your code by "pushing them to the edge of your application" like Redux does, or you add a whole new concept for control flow like the function tree does? What are some of those benefits? This article explains it well.

  1. "Simple/easy to reason about/deterministic" as in you can reliable expect the same output given the same input
  2. Easy to reuse/move around
  3. Easy to test
  4. Easy to optimize via memoization/referential transparency/lazy running/parallelization

So those are all really valuable, but by pushing side effects out you're not removing them from your program—just adding more layers/indirection to try and isolate pure functions where you can. I don't doubt that pure functions are valuable—but are they really worth it as a default? Can we make an argument that tame impure functions can do an decent job at the above 4 points? That is functions that have side-effects in the form of mostly async/IO behavior, maybe the occasional random/DOM/date effect, and not so much functions that rely on implicit shared state in the way OO does.

  1. Yes We're still ensuring unidirectional data flow where we go from input => state change => re-render. That isn't expressed directly as a pure (state, action) => newState function but it isn't creating bi-directional/implicit/stateful dependencies either. If a function does (event) => someIO(event).then((foo) => state.update(foo)) then barring exceptions from the IO we can expect our state to change in a deterministicish way.
  2. Sort of Probably more so than the Redux approach. It's probably more useful to reuse a function like searchMembers(state, term): state than it is to reuse membersReducer(state, action): state. I don't find this benefit is realized much in the places Redux uses pure functions anyways (reducers or action creators).
  3. Yes The argument for this one strikes me as particularly weak, especially for JS-land. It's quite easy to use a dependency injection lib like rewire to stub side-effect creating libraries—which you have to do anyways to test action creators. So instead of writing tests for one cohesive impure function you have to write mutliple test suites—one that is easy to test by passing data in-data out, and the other is going to be just as "difficult" to test as the former and require rewire or obscuring your app code more by requiring a kind of dependency injection technique.
  4. No This one is a simple no. Pure functions have a clear advantage when it comes to reliable use of memoization/parallelizaiton techniques. That said, I heard "premature optimization is the root of all evil" and maybe the impure functions can be refactored down the line to use these techniques when bottlnecks arrive. Unfortunately JS is not running in an environment like Erlang so we can't automatically take advantage of things like parallelization and fault-tollerance. However, if Elixirscript ever takes off I'll definitely jump on that train and purify the crap out of my functions.

In Conclusion

I'm not convinced pure functions as a default are worth the boilerplate and overhead. As described above you can choose to compose event handlers out of some pure functions with Baobab—it's just not enforced, and I think that's a good thing. Consider the benefits of pure functions—in Javascript, practically speaking, you only get to realize some of those at the cost of obscuring your code and/or adding more boilerplate and overhead.

@markerikson
Copy link

I'm legally obligated to paste a link to Dan Abramov's article "You Might Not Need Redux", where he discusses the tradeoffs involved in using Redux. The Redux FAQ entry on "When Should I Use Redux?" is also relevant.

@vcardins
Copy link

vcardins commented Mar 29, 2017

That's exactly what I was looking for for day. Thanks for such clear and elucidative comparison.

Copy link

ghost commented Apr 17, 2017

Another great resource on this topic is Preethi's talk at React Conf 2017: https://www.youtube.com/watch?v=76FRrbY18Bs&index=8&list=PLb0IAmt7-GS3fZ46IGFirdqKTIxlws7e0.

What I got from it was that Redux forces you to write code in a way that makes debuggability & testing very obvious. Hence it scales well with teams that have contributors of different skill levels.

@fgarcia
Copy link

fgarcia commented Jul 20, 2017

Given that Baobab is older than Redux I was excited noticing you were questioning both not long ago. I am in your same position now and can't avoid preferring Baobab.

I do think Redux is a technically superior approach. However doing things the "proper" way translates in lots of protocol and boilerplate. I just want to relax, play dirty and do things properly latter if they are finally intended to survive. Obviously my case is very rare since most coders are not instructed to explore, they are told exactly what must be done, close the feature, forget and move on. In those cases Redux seems a better fit

My initial preference for Baobab is based solely in that they introduce cursors/selectors at their core, with no impact in how you must implement your controllers. I find easier controlling frontend code with cursors / selectors than with actions / reducers. Maybe I am biased because I want to clearly see Events and Commands in my code, specially because I want to keep a closer coding style between frontend and backend.

Baobab enables me to keep my Events / Commands, but Redux forces me to replace them with actions / reducers.

@TobiasHauck
Copy link

Great comparison. I'm baobab user because of the fact that is doesn't need all that boilerplate stuff at all.
@fgarcia there's nothing to add. Exactly my opinion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment