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
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
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
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
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
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
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
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
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.
State Management with a Single Line of Code
Ankit Singh ・ Nov 2 '20
Cheers
🌏 ActiveJS Website
📖 ActiveJS Documentation
🤾♂️ ActiveJS Playground
💻 ActiveJS GitHub Repo (drop a ⭐ maybe :)
Top comments (3)
Gonna need a full react or angular example to really see the value in action. Sounds cool tho, the toy examples are def shorter.
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.
Thanks, for other ppl, the angular are example is normal angular till you get to the service which has the active js bits.