Skip to content

Instantly share code, notes, and snippets.

@mjackson
Last active March 17, 2023 16:53
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mjackson/e1837fad016c1c1542df34d23c4e4301 to your computer and use it in GitHub Desktop.
Save mjackson/e1837fad016c1c1542df34d23c4e4301 to your computer and use it in GitHub Desktop.
Redux, with keyspace notifications ala Redis
// I was trying to optimize some redux queries the other day when
// the thought occurred to me that it would be nice if redux let
// you know *what keys* changed instead of just letting you know that
// there was *some* kind of change.
// I was reminded of Redis' "keyspace notifications" (http://redis.io/topics/notifications)
// that can be used to get notifications about changes in the Redis
// keyspace, and I thought maybe we could try a similar idea in redux.
// So I wrote this little homegrown version of redux, with support for
// notifying subscribers exactly which keys have changed. See the usage.js
// file for how you might use this.
const initAction = { type: '@@redux/INIT' }
const isEmptyChanges = (changes) => {
for (let prop in changes)
return false
return true
}
const combineReducers = (reducers) => {
const keys = Object.keys(reducers).filter(key => typeof reducers[key] === 'function')
return (state = Object.create(null), action, changes = Object.create(null)) => {
let hasChanges = false
const nextState = keys.reduce((memo, key) => {
const reducer = reducers[key]
const keyChanges = Object.create(null)
const prevState = state[key]
const nextState = reducer(prevState, action, keyChanges)
if (prevState !== nextState) {
memo[key] = nextState
changes[key] = isEmptyChanges(keyChanges)
? (nextState == null ? 'removed' : (prevState == null ? 'added' : 'updated'))
: keyChanges
hasChanges = true
} else {
memo[key] = prevState
}
return memo
}, Object.create(null))
if (hasChanges || action === initAction)
return nextState
return state
}
}
const createStore = (reducer, initialState) => {
let state = reducer(initialState, initAction)
let listeners = []
const subscribe = (listener) => {
listeners.push(listener)
return () => {
listeners = listeners.filter(item => item !== listener)
}
}
const dispatch = (action) => {
const changes = Object.create(null)
const nextState = reducer(state, action, changes)
if (nextState !== state) {
state = nextState
listeners.forEach(listener => {
listener(changes)
})
}
}
const getState = () =>
state
return {
subscribe,
dispatch,
getState
}
}
// First, let's try a simple counter. This is a simple reducer
// that doesn't know or care about changes; it's just a number.
let store = createStore((state = 0, action) => {
return action.type === 'increment' ? state + 1 : state
})
store.subscribe(changes => {
// Let's log out the "changes" every time something changes.
// It's an empty array every time. The store has no keys!
console.log(changes)
})
store.dispatch({ type: 'increment' })
store.dispatch({ type: 'increment' })
store.dispatch({ type: 'increment' })
// Now let's try some nested data. Here we've got a "user" state
// with a "firstName" and a "lastName". It would be great if we
// could be notified when "user.firstName" or "user.lastName" changes,
// or any time the "user" state changes.
const reducer = combineReducers({
count: (state = 0, action) => {
return action.type === 'increment' ? state + 1 : state
},
user: combineReducers({
firstName: (state = null, action) => {
return action.type === 'update-first-name' ? action.name : state
},
lastName: (state = null, action) => {
return action.type === 'update-last-name' ? action.name : state
}
})
})
store = createStore(reducer)
store.subscribe(changes => {
// Let's log out the changes every time something changes. We'll get:
// { count: 'updated' }
// { count: 'updated' }
// { count: 'updated' }
// { user: { firstName: 'added' } }
// { user: { lastName: 'added' } }
// { user: { lastName: 'removed' } }
console.log(changes)
})
// Counter still works great. Now that our state is an object with
// a "count" key (instead of just a plain integer) we will get notified
// that the "count" key changed in our subscribers. Subscribers who don't
// care about the "count" key can safely ignore those changes.
store.dispatch({ type: 'increment' })
store.dispatch({ type: 'increment' })
store.dispatch({ type: 'increment' })
// Nested state changes work great too!
// In this action, subscribers are notified that both the "user"
// and the "user.firstName" keys have changed.
store.dispatch({ type: 'update-first-name', name: 'Michael' })
// Likewise, in this action, subscribers are notified that the "user"
// and "user.lastName" keys have changed. In both cases, subscribers that
// only care about the "count" state can safely ignore these changes.
store.dispatch({ type: 'update-last-name', name: 'Jackson' })
// We can even get notified when a key is removed.
store.dispatch({ type: 'update-last-name', name: null })
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment