I've been spending some free time refactoring an OSS project I took over when the original dev sold the Chrome store app to a malware company; users have been asking for new features.
It was suffering from severe bitrot (react v0.12) and a raft of antipatterns (forceUpdate * 20) so I figured it would be a good candidate to help people and sharpen my frontend skills at the same time.
As I was working on some refactoring I noticed that some of my code was beginning to get rather terse and difficult to reason about. Terseness is nice, but code that can't be maintained is a liability, not an asset.
So I decided to kata on a single piece of functionality to help polish my feel for balance in the force.
// Tab was closed, remove tab from models if !isWindowClosing
chrome.tabs.onRemoved.addListener((rid, ri) => ri.isWindowClosing || setCache(cache =>
cache.map(w => ({ ...w, ...w.id === ri.windowId && { tabs: w.tabs.filter(t => t.id !== rid) } }))
))
Almost maximum expansion of the same logic, and much more readable. However, I realized that I also need some of this functionality in a few other places.
// Tab was closed
chrome.tabs.onRemoved.addListener((tabid, removeInfo) => {
// If the window is closing, window handler will nuke this data
if(!removeInfo.isWindowClosing) {
// Update the cache state by filtering out the removed tab
setCache(wincache => wincache.map(window => {
if(window.id === removeInfo.windowId) {
window.tabs = window.tabs.filter(tab => tab.id !== tabid)
}
return window
}))
}
})
As I began to extract a couple blobs of the logic for reuse, I started feeling again the pull to the dark side of functional programming.
// Maps a function on a parameter according to a predicate, identity otherwise
const mapIf = (pred, func) => param => pred(param) ? func(param) : param
// Takes an object and filters a specd array property given a predicate
const filterProp = (prop, pred) => obj => ({ ...obj, [prop]: obj[prop].filter(pred) })
// Takes a window and filters its tabs based on a predicate
const filterTabsProp = pred => filterProp('tabs', pred)
// Update the cache state by filtering out the removed tab
const removeTab = (windowid, tabid) =>
setCache(cache => cache.map(
mapIf(
window => window.id === windowid,
filterTabsProp(tab => tab.id !== tabid)
)
))
// Tab was closed
chrome.tabs.onRemoved.addListener((tabid, removeInfo) =>
// If the window is closing, window handler will nuke this data
removeInfo.isWindowClosing || removeTab(tabid, removeInfo.windowId)
)
Hrrrm... npm i ramda
import { pipe, when, lensProp, over, reject } from 'ramda'
// Lens to the tabs object property for a window model
const tabsLens = lensProp('tabs')
// Execute some operation over the tabs lens
const overTabs = over(tabsLens)
// Update the cache by filtering out the removed tab
const removeTab = tab => map(
when(
window => window.id === tab.windowId,
overTabs(reject(t => t.id === tab.id))
)
)
// Tab was closed
chrome.tabs.onRemoved.addListener((tabid, removeInfo) =>
// If the window is closing, window handler will nuke this data
removeInfo.isWindowClosing ||
setCache(removeTab({ id: tabid, windowId: removeInfo.windowId }))
)
📷: https://www.feastingathome.com/stuffed-butternut-three-ways