/reless-js

Relessjs = Redux - Actions. Call the reducer directly. Redux without the maintenance. No more need to update Actions, Names, Constants, and Reducers' switch-cases in multiple places.

Primary LanguageJavaScript

Reless-js

A state management library, inspired by Flux architecture and the Redux implementation.

This implementation slightly deviates from the Flux spec, and doesn't work with actions and dispatching of actions. In Reless you can semi-directly call a reducer with some payload, while still keeping a single state, the unidirectional dataflow and possibilities to use the Redux-devtools.

Concepts

Flux

Flux architecture is built around unidirectional data-flow.

You dispatch an action, that's is being reduced on the current state. After all reducers finished doing their job, the listeners are called to notify that there was a potential change in the state.

Maintenance with Redux

skip to Relessjs (to the rescue) if you want to know what Reless can do for you.

The main thing that troubles me is the maintenance when using Redux. (If you know Redux and want to skip to the real deal of this library: )

When creating actions you're not just calling dispatch({ type: 'INCREMENT_COUNTER' }), but you would define a constants file where you define all actions:

module.exports = {
  incrementCounter = 'INCREMENT_COUNTER'
}

Now you can call use the constant instead of typing it, and it's in one place to maintain. The result:

let constants = require('./constants.js')

dispatch({ type: constants.incrementCounter })

But wait, we might have a very complex action, where, based on some properties we'd like to convert it into another action. So let's create an actionFactory, and let's not forget to import the constants:

let constants = require('./constants.js')

module.exports = {
  increment: (someData) => {
    return { 
      type: constants.incrementCounter, 
      data: someData.map(x => x.toLowerCase()) 
    }
  }
}

But wait, we haven't even talked about the reducer yet:

let constants = require('./constants.js')

module.exports = {
  counter: (state, action) {
    if (typeof state === 'undefined') {
      return { ...state, counter: 0 }
    }
    switch (action.type) {
      case constants.incrementCounter:
        return { ...state, counter: state.counter + 1 }
      case constants.decrementCounter:
        return { ...state, counter: state.counter - 1 }
      default:
        return state
    }
  }
}

This process is a bit cumbersome. If you want a bit of functionality you have to change 3 files and this grows when you have more and bigger actions and reducers. Maybe this is not a problem for a big enterprise-like application but for something small, you'd like something more to the point. This is where Relessjs can help.

Relessjs (to the rescue)

Have a look here to see the basic scenarios test/showcase

So... You still want to use a single state, but looking at these 4 files, Redux might be overkill. Now you can use Relessjs to create a store, call reducers "directly" and update the state in a unidirectional flow.

With Relessjs we can give you exactly that, but with fewer files and no constants. Your reducer is your action. So you call your reducer, and watch the state change.

Example without and with state in the reducer

Let's give a simple example of a counter:

  • setCountToOne: call a reducer
  • setCount: call a reducer with a payload
  • incrementCounter: call a reducer, and use the previous state

The simplest form of the reducer is of type payload => (state | (state => state)).

The reducer can be called with a payload. Then you can directly return a state or return a function of type state => state. When you return a function from the state, it'll be called with the current state. You can use this state to increment a counter.

let store = new Reless({
  state: { counter: 0 },
  reducers: { 
    setCountToOne: () => ({ counter: 1 }),
    setCount: (count) => ({ counter: count }),
    incrementCounter: () = state => ({ counter: state.counter + 1 }),
  },
})

store.reducers.setCountToOne()
store.state.counter // 1

store.reducers.setCount(4)
store.state.counter // 4

store.reduces.incrementCounter()
store.state.counter // 5

Example with asynchronous call in reducer

If you want to do something asynchronous, your reducer should be of type payload => state => reducers => void

Here the reducers get passed on. You can call them inside another reducer. This way we can keep track of the reducer calls.

We've had some prior art, where we passed an update function, but we experienced an issue with that: you cannot log what is happening since you only pass a new state instead of calling a reducer. In terms of Redux, you want to dispatch an action and not directly set a state, so that you have a nice trail of actions that are dispatched in order to see where bugs are happening.

let store = new Reless({
  state: { counter: 0 },
  reducers: { 
    setLoading: loading => ({ loading })
    doAsync: () => () => (reducers) => {
      reducers.setLoading(true)
      setTimeout(() => {
        reducers.setLoading(false)
      }, 1000)
    }
  },
})

store.reducers.doAsync()
store.state.loading // true
// one second later
store.state.loading // false

Asynchronous example using the newest state

When returning a function in the reducer call, it'll be called with the latest state. This will allow you to create a countdown based on the last state.

The function passed to the reducer has the following type: state => state.

  let store = new Reless({
    state: { counter: 3 },
    reducers: {
      setCounter: (counter) => ({ counter })
      countdown: () => () => reducers => {
        let interval = setInterval(() => {
          reducers.setCounter(state => { 
            if (state.counter === 1) {
              clearInterval(interval)
              return 0
            }
            return state.counter - 1
          })
        }, 1000)
      },
    },
  })
  // start with 3
  store.reducers.countdown()
  // directly after calling the reducer
  store.state.counter // 3
  // first second passes
  store.state.counter // 2
  // second second passes
  store.state.counter // 1
  // third second passes
  store.state.counter // 0