DEV Community

loading...
Cover image for State Management without Reducers

State Management without Reducers

dabalyan profile image Ankit Singh ・4 min read

If you're a developer like me who has problems connecting the disconnected abstract pieces of code and have a hard time understanding how and where the state is being updated. Then you might also be hitting the limits of the human brain's limited working memory.

I think I have found a solution, a way to get rid of reducers without losing any benefits. During the last 10 months, I built an efficient state manager called ActiveJS that doesn't use reducers.

Instead of dispatching actions and let reducers implicitly work their magic, we just dispatch reducer like pure-functions and get rid of actions in the process; two birds with one stone.

These pure-functions are called producers because they take the current state and produce a new state.

So reducers reduce, and producers produce, but essentially they are doing the same thing, updating the state. Sounds good, right?

Let's put the theory to test, and write some code:

State Management with Reducers

First, let's look at the good old reducers.
This is how a simple counter looks like, implemented with the champion of reducers, Redux.

const counter = (state, action) => {
  if (typeof state === 'undefined') {
    return 0
  }

  // these are our reducers
  switch (action.type) {
    case 'INCREMENT':
      return state + 1
    case 'DECREMENT':
      return state - 1
    default:
      return state
  }
}

// initialize the store and bind the reducers
const store = Redux.createStore(counter)

// subscribe for the state changes and log it to console
store.subscribe(() => console.log(store.getState()));
// logs 0 immediately and will log any future values

// dispatch the action for increment
store.dispatch({ type: 'INCREMENT' }) // makes the count 1

// dispatch the action for decrement
store.dispatch({ type: 'DECREMENT' }) // makes the count 0
Enter fullscreen mode Exit fullscreen mode

Now let's see what happens when we replace reducers with producers.

State Management with Producers

For this, we'd use ActiveJS, the new kid on the block, it's got built-in reactive data structures called Units, that store and provide native data structures as value, like number, string, array, etc.

One of those Units is NumUnit, it stores a number value and ensures that it stays a number, even NaN isn't allowed.

We'd use the NumUnit to implement our counter because we expect the count to always be a number.

// initialize a reactive data structure to store numbers
const counter = new NumUnit() // with default initial-value 0

// two producers, pure-functions to produce an appropriate new value
const increment = value => value + 1 
const decrement = value => value - 1

// subscribe for reactive value access, and log the value
counter.subscribe(value => console.log(value))
// immediately logs 0, and will log any future values

// dispatch the "increment" producer for increment
counter.dispatch(increment); // you'll see 1 in the console
// the pure function is called with the current value and
// the returned value is dispatched automatically

// dispatch the "decrement" producer for decrement
counter.dispatch(decrement); // you'll see 0 in the console
Enter fullscreen mode Exit fullscreen mode

Look ma, No Reducers!

Easy right?

What about actions with payload?

Let's say we want to increment the current value after multiplying it with the provided number as payload, let's see how that producer would look like.

const multiplyAndIncrement = multiplier => {
  // the wrapper function acts as an action, and
  // this is our producer now
  return value => value * multiplier + 1
}

// assume the current value of the counter is 2, for easy demonstration

// dispatch the producer
counter.dispatch(multiplyAndIncrement(3))
// the new value becomes 7, because 2 * 3 + 1 is 7

// we call multiplyAndIncrement function and it returns the producer-function
// the dispatch method calls the returned producer with the current value
// the returned value from the producer is used as new value
Enter fullscreen mode Exit fullscreen mode

The theory checks out, the code is still functional, easily testable, and the flow of the code is not broken.

But producers are just one of the good things about Units, there are so many features baked in.

Here are some of those features that are relevant for our "counter":

Direct Dispatch

If you don't have a need to use producers, you can just dispatch the new value directly.

counter.dispatch(2) // works
counter.dispatch(3) // works
Enter fullscreen mode Exit fullscreen mode

Direct Value Access

If you are not doing something reactively, and just need the value, you can access it directly.

// static value access
counter.value() // returns the current value immediately
Enter fullscreen mode Exit fullscreen mode

Preventing Invalid Data Types

For this we don't have to do anything, NumUnit takes care of that.
It only takes in a number, which ensures that it always provides a number value. Saves you the need for any sanity checks.

counter.dispatch('an evil string') // won't work
counter.dispatch({nein: 'nein nein'}) // won't work
counter.dispatch(NaN) // won't work
counter.dispatch(() => NaN) // won't work
counter.dispatch(['what', 'about', 'this']) // won't work
Enter fullscreen mode Exit fullscreen mode

Preventing Duplicate Dispatch

Preventing duplicate values is as easy as providing a configuration option.

const counter = new NumUnit({distinctDispatch: true})
// that's it, done

counter.dispatch(2) // works
counter.dispatch(2) // won't work, it'll get ignored

counter.dispatch(3) // works
counter.dispatch(3) // won't work, it'll get ignored
Enter fullscreen mode Exit fullscreen mode

Preventing Negative Values

It makes sense that our counter should only have positive values. And making sure of that is also easy by providing a function, it'll be called by the Unit to check if the new value should be updated or not.

const counter = new NumUnit({
 // If it returns a falsy value for a dispatch, that dispatch is ignored.
 // So just return true if the newly dispatched value is positive.
  customDistinctDispatch: (prev, newValue) => newValue >= 0
})
// that's it, done

counter.dispatch(2) // works
counter.dispatch(-2) // won't work, it'll get ignored

counter.dispatch(3) // works
counter.dispatch(-3) // won't work, it'll get ignored
Enter fullscreen mode Exit fullscreen mode

That's it, folks, for now.

Here's the StackBlitz playground link if you want to try it out yourself.

There are even more things that NumUnit alone and Units at large can do. Here's an article demonstrating some of those features.

Cheers

🌏 ActiveJS Website
📖 ActiveJS Documentation
🤾‍♂️ ActiveJS Playground
💻 ActiveJS GitHub Repo (drop a ⭐ maybe :)

Discussion (4)

pic
Editor guide
Collapse
luisbedoya13 profile image
Luis Bedoya

I just read all the docs and it's simply cool, gotta use it with a personal project

Collapse
sirseanofloxley profile image
Sean Allin Newell

Gonna need a full react or angular example to really see the value in action. Sounds cool tho, the toy examples are def shorter.

Collapse
dabalyan profile image
Ankit Singh Author

hey Sean, good question, I added an example for action with payload, hope that helps. Please let me know if something is still missing.

For a complexer example please take a look at the TodoMVC example on the website.

Collapse
sirseanofloxley profile image
Sean Allin Newell

Thanks, for other ppl, the angular are example is normal angular till you get to the service which has the active js bits.