Immer vs Ramda - two approaches towards writing Redux reducers
Tags: react, redux, webdev, javascript
Published: 2021-02-20 15:13:55

**Reducers** - a core element of **Redux**'s philosophy that tightly **grabs** mutations of a given state in **one** place. In theory, the **pure** nature of reducers should lead to great **scalability**, **readability**, and make us all fortunate children of Redux god. But even the **brightest** idea can be dimmed if thrown on the one most **pediculous** soil...

Yes. I speak about **JavaScript**. Writing complex pure functions in **vanilla** JavaScript is **harsh**. Avoiding **mutations** is extraordinarily **hard**. Matching against **actions**? There are no **Variants**/**Enums** in **JS**, you have to use **strings** instead. And you land with a **poor** switch statement taken straight from the hell. Regardless, Redux is the most **popular** state manager for **React** applications

## The path to purity

Consider the two ways to make your life easier, the first one will be the **Immer** - **Immer** is a package that lets you deliver the next **state** by "mutating" the **draft** of the **previous** state:

```js
import produce from 'immer'

const replace = produce((draft, key, element) => {
  draft[key] = element
})

const list = ['⚾', '🏀', '🏉']
const newList = replace(list, 1, '⚽')
```

The `replace` function is **pure**, despite the explicitly written assignment of property. It **does not** change the **original** object. So with little help of the `produce` function, you can write **mutating** logic inside your reducer.

The second way is to use the **Ramda** library. **Ramda** is a **set** of utility **functions** that perform basic operations on data and functions. And all of them are **pure**!

```js
import { update } from 'ramda'

const list = ['⚾', '🏀', '🏉']
const newList = update(1, '⚽', list)
```

### The **Immer** Way

Let's get to work and write a simple "todo" reducer with **Immer**:

**_Warning drastic content!_**

```js
const todosRedcuer = produce((state, action) => {
  const isTodo = todo => todo.id === action.todo?.id
  const remove = (index, arr) => arr.splice(index, 1)

  switch (action.type) {
    case 'ADD_TODO':
      state.unshift({ ...action.todo, id: generateID() })
      break
    case 'CHECK_TODO':
      for (const index in state) {
        if (isTodo(state[index])) {
          state[index].done = !state[index].done
          break
        }
      }
      break
    case 'REMOVE_TODO':
      for (const index in state) {
        if (isTodo(state[index])) {
          remove(index, state)
          break
        }
      }
      break
    case 'EDIT_TODO':
      for (const index in state) {
        if (isTodo(state[index])) {
          state[index].text = action.next.text
          break
        }
      }
      break
    default:
  }
})
```

It's **disgusting**. There are **so** much **code** and so **little** **meaning** in this example. It's **under-engineered**. Our code does not have to be so **procedural**. Let's refactor it to be consumable:

```js
const todosRedcuer = produce((state, action) => {
  const isTodo = todo => todo.id === action.todo?.id
  const not = fn => v => !fn(v)
  const todoIndex = state.findIndex(isTodo)

  switch (action.type) {
    case 'ADD_TODO':
      state.unshift({ ...action.todo, id: generateID() })
      break
    case 'CHECK_TODO':
      state[todoIndex].done = !state[todoIndex].done
      break
    case 'REMOVE_TODO':
      return state.filter(not(isTodo))
    case 'EDIT_TODO':
      state[todoIndex].text = action.next.text
      break
    default:
  }
})
```

Much **better**. Now you can see the **benefits** of **Immer**. We can **freely** use **well-known** methods like `push` `pop` `splice`, we can **explicitly** assign new values. And If it's in your need, you can return from `produce` and it will behave as a **regular** function (See the `REMOVE_TODO` action).

> @markerikson - the maintainer of **Redux**. Advised me that if you consider using __Immer__ to write __Redux__ reducers you should go with [Redux ToolKit](https://redux-toolkit.js.org) - the official way to **reduce** (😎) **Redux** boilerplate 

### The die has been cast - **Ramda** way

Let's recreate the **same** functionality, this time utilizing the power of **Ramda**:

```js
const reducer = pipe(uncurryN(2), flip)

const todosRedcuer = reducer(action => {
  const lensTodo = pipe(indexOf(action.todo), lensIndex)
  const lensTodoProp = (prop, state) => compose(lensTodo(state), lensProp(prop))

  switch (action.type) {
    case 'ADD_TODO':
      return prepend({ ...action.todo, id: generateID() })
    case 'CHECK_TODO':
      return state => over(lensTodoProp('done', state), v => !v, state)
    case 'REMOVE_TODO':
      return without([action.todo])
    case 'EDIT_TODO':
      return state => set(lensTodoProp('text', state), action.next.text, state)
    default:
      return identity
  }
})
```

If you wonder - it's not even worth reading. This code is **complex** and **stupid** at the same time - it's over-engineered. When I had **written** this I've **realized** I've got too far. Let's refactor it:

```js
const reducer = pipe(uncurryN(2), flip)

const todosRedcuer = reducer(action => {
  const findTodo = indexOf(action.todo)
  const evolveTodo = ev => state => adjust(findTodo(state), evolve(ev), state)

  switch (action.type) {
    case 'ADD_TODO':
      return prepend({ ...action.todo, id: generateID() })
    case 'CHECK_TODO':
      return evolveTodo({ done: v => !v })
    case 'REMOVE_TODO':
      return without([action.todo])
    case 'EDIT_TODO':
      return evolveTodo({ text: () => action.next.text })
    default:
      return identity
  }
})
```

### **Ramda** functions

Let’s walk through each of these functions:

Before it's too late. All **Ramda** functions are **curried** in the sense that we can **partially** apply arguments to them. So instead:
```js
const add5 = n => add(5, n)
```
We can do:
```js
const add5 = add(5) // If add is curried
```

#### `pipe`

It allows you to compose functions such as the product of the first function becomes an argument of the second one and so on. It reduces the noise when composing functions. And this:

```js
pipe(uncurryN(2), flip)
```

Is equivalent to this:

```js
fn => flip(uncurryN(2, fn))
```

Besides, there is `compose` function in **Ramda** set. It works exactly the same but in reverse order:

```js
compose(flip, uncurryN(2))
```

#### `uncurryN`

It transforms curried arguments of function to standard one. So:

```js
const curriedPower = a => b => a ** b

const power = uncurryN(2, curriedAdd)
power(3, 2) // Returns: 9
```
> **Why?** I want to use the curried function like `action => state => next_state`. But **Redux** expects the reducer to be `(state, action) => next_state`

#### `flip`

It swaps the first two arguments of the given function:

```js
const flipPower = flip(power)

flipPower(3, 2) // Returns: 8
```

> **Why?** If you didn't notice, reducer arguments are reversed. We have `(action, state) => ...` and flip it to `(state, action) => ...`

#### `indexOf`
Works similarly to `Array.proptotype.indexOf` with the difference that it matches objects too:

```js
indexOf('🐟', ['🦍', '🐖', '🐟'])
```

You could use `findIndex` to achieve the same effect. It's `Array.prototype.findIndex` exposed as curried function:

```js
const isFish = animal => animal === '🐟'

findIndex(isFish, ['🦍', '🐖', '🐟'])
```

It's the same as:

```js
;['🦍', '🐖', '🐟'].findIndex(isFish)
```

#### `equals`

> It's how `indexOf` and other **Ramdas** functions compare arguments

This function compares two values:

```js
const isFish = equals('🐟')
```

It's a **deep comparison** so you can compare objects as well:

```js
equals([1, 2], [1, 2]) // Returns: true
```

#### `adjust`

Adjust applies the function to a specific element of the array

```js
adjust(1, n => n * 2, [1, 2, 3]) // Returns: [1, 4, 3]
```

> **Why?** It's how we aim at specific todo to change it further

#### `evolve`

One of my favorite functions. It takes the object reducers and applies them for corresponding properties:

```js
const player = {
  level: 4,
  gold: 1858,
  mana: 3000,
}

evolve(
  {
    mana: m => m + 2,
    gold: g => g + 1,
  },
  player
) // Returns: { level: 4, gold: 1859, mana: 3002 }
```

#### `prepend`

Works as `Array.prototype.unshift` but returns a new array instead of modifying the existing one

#### `without`

It takes the list of elements and array and returns a new array without them. It uses `equals` to compare elements so you can exclude objects too.

```js
without(['👞', '👢'], ['👞', '👟', '🥿', '👠', '👢']) // Returns: ['👟', '🥿', '👠']
```

#### `identity`

It's just:

```
v => () => v
```

> **Why?** By default we want to return the previous state without any change

## Conclusion

Both **Immer** and **Ramda** are great tools to keep purity in js. The big benefit of **Immer** over **Ramda** is the fact that you don't have to learn anything new - just use all your JavaScript knowledge. What is more, changes inside `produce` are very **clear**. **Ramda** gives you the **right** functions to do the job, as a result, your code becomes **less** repetitive, **clean**, and very **scalable**. Of course, you can write all of those functions by yourself, but what is the point of reinventing the wheel? What is the reason to use patterns? If there is a pattern, then there is a place for automation. Nevertheless, these packages can be easily **abused**. While your code can be too procedural the wrong abstraction may be just as big overhead.

> As some others have noticed this "comparison" is not very accurate, since __Ramda__ and __Immer__ are not strict competitors. You'll never stand before the choice "Which one to use over another". It was apparent to me - this form I've chosen is supposed to make the article more entertaining. I don't pressure you to make any choice