Last active
March 17, 2023 16:53
-
-
Save mjackson/e1837fad016c1c1542df34d23c4e4301 to your computer and use it in GitHub Desktop.
Redux, with keyspace notifications ala Redis
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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