Simplified Redux
Sultan
Posted on November 3, 2023
In the previous article, Declarative JavaScript, we discussed how conditional flows can be represented as higher-order functions. In this chapter, we will continue to extend this concept to create Redux reducers. Despite its declining popularity, Redux can serve as a good example of how to reduce the boilerplate code around this library. Before proceeding, I highly recommend reading the first chapter.
Abstractions
You may have noticed that whenever you point at some object, if there is a dog alongside you, it will most likely just stare at your fingertip rather than the object itself. Dogs struggle to understand the abstract line between the finger and the intended object, because such a concept simply does not exist in their cognition. Fortunately, humans have this ability, and it allows us to simplify complex things and present them in a simple form.
In the diagram above, a blue square symbolizes an application, and a barrel attached to the box represents a database. Such diagrams help us understand complex architectural solutions without diving into the details of their implementation. The same method can be applied in programming by separating the main code from the secondary code by hiding them behind functions.
const initialState = {
attempts: 0,
isSigned: false,
}
// action payload π π current state
const signIn = pincode => state => ({
attempts: state.attempts + 1,
isSigned: pincode === '2023',
})
const signOut = () => () => initialState
const authReducer = createReducer(
initialState,
on('SIGN_IN', signIn),
on('SIGN_OUT', signOut),
on('SIGN_OUT', clearCookies),
)
The input of the createReducer
function is like the table of contents for a book. It allows us to quickly finding the necessary function based on the action type. Both signIn
and signOut
functions update the state object, accepting the action payload and the current state as input. The rest of the code, which involves the action type check and the reducer call, is encapsulated within the createReducer
and on
functions.
const createReducer = (initialState, ...fns) => (state, action) => (
fns.reduce(
(nextState, fn) => fn(nextState, action),
state || initialState,
)
)
const on = (actionType, reducer) => (state, action) => (
action.type === actionType
? reducer(action.payload)(state)
: state
)
For enhanced utility, we introduce the helper functions useAction
and useStore
:
import {useState} from 'react'
import {useDispatch, useSelector} from 'react-redux'
const useAction = type => {
const dispatch = useDispatch()
return payload => dispatch({type, payload})
}
const useStore = path => (
useSelector(state => state[path])
)
const SignIn = () => {
const [pincode, setPincode] = useState('')
const signIn = useAction('SIGN_IN')
const attempts = useStore('attempts')
return (
<form>
<h1>You have {3 - attempts} attempts!</h1>
<input
value={pincode}
onChange={event => setPin(event.target.value)}
/>
<button
disabled={attempts >= 3}
onClick={() => signIn(pincode)}>
Sign In
</button>
</form>
)
}
Lenses
In functional programming, lenses are abstractions that make operations easier with nested data structures, such as objects or arrays. In other words, they are immutable setters and getters. They are called lenses because they allow us to focus on a specific part of an object.
First, let's see how to update a value without using lenses:
const initialState = {
lastUpdate: new Date(),
user: {
firstname: 'Peter',
lastname: 'Griffin',
phoneNumbers: ['+19738720421'],
address: {
street: '31 Spooner',
zip: '00093',
city: 'Quahog',
state: 'Rhode Island',
}
}
}
const updateCity = name => state => ({
...state,
user: {
...state.user,
address: {
...state.user.address,
city: name,
}
}
})
const userReducer = createReducer(
initialState,
on('UPDATE_CITY', updateCity),
...
)
Thatβs pretty ugly, right? Now let's use the set/get
lenses:
const updateCity = name => state => (
set('user.address.city', name, state)
)
const updateHomePhone = value => state => (
set('user.phoneNumbers.0', value, state)
)
const useStore = path => useSelector(get(path))
// or by composing
const useStore = compose(useSelector, get)
const homePhone = useStore('user.phoneNumbers.0')
Below is an implementation of the lenses. I should note that all of these functions are already available in the Ramda library.
const update = (keys, fn, obj) => {
const [key, ...rest] = keys
if (keys.length === 1) {
return Array.isArray(obj)
? obj.map((v, i) => i.toString() === key ? fn(v) : v)
: {...obj, [key]: fn(obj[key])}
}
return Array.isArray(obj)
? obj.map((v, i) => i.toString() === key ? update(rest, fn, v) : v)
: {...obj, [key]: update(rest, fn, obj[key])}
}
const get = value => obj => (
value
.split(`.`)
.reduce((acc, key) => acc?.[key], obj)
)
const set = (path, fn, object) => (
update(
path.split('.'),
typeof fn === 'function' ? fn : () => fn,
object,
)
)
const compose = (...fns) => (...args) => (
fns.reduceRight(
(x, fn, index) => index === fns.length - 1 ? fn(...x) : fn(x),
args
)
)
Curring functions
A curried function can be referred to as a function factory. We can assemble a function from different places, or we can use them in composition with other functions. This is a truly powerful feature. However, when we call a curried function with all parameters at once, it may look clumsy. For instance, the call of the curried version of set
would look like this:
set('user.address.city')(name)(state)
Let's introduce a curry
function. This function converts any given function into a curried one, and it can be invoked in various ways:
set('user.address.city', 'Harrisburg', state) // f(x, y, z)
set('user.address.city', 'Harrisburg')(state) // f(x, y)(z)
set('user.address.city')('Harrisburg', state) // f(x)(y, z)
set('user.address.city')('Harrisburg')(state) // f(x)(y)(z)
Quite simple implementation and usage:
const curry = fn => (...args) => (
args.length >= fn.length
? fn(...args)
: curry(fn.bind(undefined, ...args))
)
const set = curry((path, fn, object) =>
update(
path.split('.'),
typeof fn === 'function' ? fn : () => fn,
object,
)
)
const get = curry((value, obj) =>
value
.split(`.`)
.reduce((acc, key) => acc?.[key], obj)
)
I often notice that many developers, for whatever reason, wrap callback functions in anonymous functions just to pass a parameter.
fetch('api/users')
.then(res => res.json())
.then(data => setUsers(data)) // π no need to wrap
.catch(error => console.log(error)) // π no need to wrap
Instead, we can pass the function as a parameter. We should remember that the then
function expects a callback function with two parameters. Therefore, direct passing will be safe only if setUsers
expects only one parameter.
fetch('api/users')
.then(res => res.json())
.then(setUsers)
.catch(console.log)
This reminds me of simplifying fractions or equations in basic algebra.
.then(data => setUsers(data))
// π it equals π
.then(setUsers)
Let's simplify the updateCity
function:
const updateCity = name => state => (
set('user.address.city', name, state)
)
// π it equals π
const updateCity = set('user.address.city')
Or, we can place it directly in the reducer without declaring a variable.
const userReducer = createReducer(
initialState,
on('UPDATE_CITY', set('user.address.city')),
on('UPDATE_HOME_PHONE', set('user.phones.0')),
...
)
The most important aspect is that we can now compose the set
function and perform multiple updates at once.
const signIn = pincode => state => ({
attempts: state.attempts + 1
isSigned: pincode === '2023',
})
// 'state => ({ ...' is π removed
const signIn = pincode => compose(
set('attempts', attempts => attempts + 1),
set('isSigned', pincode === '2023'),
)
const updateCity = name => state => ({
lastUpdate: new Date(),
user: {
...state.user,
address: {
...state.user.address,
city: name,
}
}
})
// π it equals π
const updateCity = name => compose(
set('lastUpdate', new Date()),
set('user.address.city', name),
)
This programming style is called point free and is widely used in functional programming.
This article ended up being a bit longer than I had initially planned, but I hope youβve gained some new knowledge. In the next article, we will touch on the topic of the try/catch
construct and as usual, here's a small teaser for the next post:
const result = trap(unstableCode)
.pass(x => x * 2)
.catch(() => 3)
.release(x => x * 10)
P.S.
You can check out the online demo here πΊ.
For a demo with TypeScript, visit this online demo π.
Posted on November 3, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
March 29, 2020