DEV Community

Cover image for Build an async redux-like useStore() hook from scratch
Andrew March
Andrew March

Posted on

2 1

Build an async redux-like useStore() hook from scratch

Flux allows us to use global state in our applications, and interact with it using actions. Redux uses strings to denote actions, switch statements to parse the dispatch commands, and is synchronous.

Here is an implementation where we do not need to use strings, or switch, and which is async compatible.

The keys object has properties on it for each variable name in our store, and the actions object has a method for each action.

import {useStore, keys} from './useStore.js'
const ColoredTextBox = () => {
const [{text, color}, actions] = useStore([keys.text, keys.color])
return(
<div
style={{backgroundColor:color}}
onClick={()=>actions.setColor(color=='red'?'green':'red')}
>
{text}
</div>
)
}

We create our store in a separate file like this. The methods on the actions object use a shorthand notation here, each action must return the keys (or an array of keys) that it has mutated, so that the dispatch function knows which listeners to call.

We can also import the emit function to trigger state updates inside our async actions.

import { createStore, getKeys, emit } from './createStore.js'
const store = {
color: 'red',
text: 'default text',
todos: [
'make an async store',
'publish medium article'
],
loading: false
}
export const keys = getKeys(store)
const actions = {
setColor(c){
store.color = c
return(keys.color)
},
setTextandColor(t,c){
store.text = t
let ks = this.setColor(c)
return([keys.text, ks])
},
addTodo(t){
store.todos.push(t)
return(keys.todos)
},
async delayedSetColor(c){
store.loading = true
emit(keys.loading)
await new Promise(resolve => setTimeout(resolve, 1000))
store.color = c
store.loading = false
return([keys.color, keys.loading])
}
}
export const useStore = createStore(store, actions)
view raw useStore.js hosted with ❤ by GitHub

createStore.js looks like this. Let me know what you think in the comments. This implementation works well with async actions, using async/await notation in the action objects methods.

The makeKeys function means there are less bugs caused by misspelled strings.

import { useState, useEffect } from 'react'
var store = {}
var actions = {}
var listeners = {}
export const emit = keys => {
// for each key that needs its listeners called
flat(keys).forEach(key=>{
// for each listener in that array
listeners[key].forEach(f=>{
// copy the value to ensure react sees the update, and pass it to the listener
const val = store[key]
if(val instanceof Array){
f([...val])
} else if(val instanceof Object){
f({...val})
} else {
f(val)
}
})
})
}
export const createStore = ( newStore, newActions ) => {
store = newStore
Object.keys(newActions).forEach(action=>{
// register each action, adding the emit function
const newAction = async (...args) =>{
// get the list of store values that need updating, by calling the action (which returns the keys it needs updated)
// await the return value incase it is a promise
// then pass it to the emit function
const keys = await newActions[action](...args)
emit(keys)
}
actions[action] = newAction
})
// initialize an empty array in the listeners object for each state variable
Object.keys(store).forEach(key=>listeners[key]=[])
//build the useStore function
const useStore = keys => {
var state = {};
flat(keys).forEach(key=>{
// get the observable value, and the update function from the useState hook.
const [val,update] = useState(store[key])
// when component mounts, register a listener with the update function, remove it when it unmounts.
useEffect(
()=>{
listeners[key].push(update)
return(()=>listeners[key] = listeners[key].filter(l=>l != update))
},
[]
)
// place the observable value in the state object
state[key] = val
})
return [state,actions]
}
return useStore
}
export const getKeys = store => {
const keys = {}
// return an object that has the state variables name as the key and the string as the value
Object.keys(store).forEach(key=>keys[key]=key)
return keys
}
// helper to convert a primitive, or an n-dimensional array, to a flat array
const flat = x => [].concat(x).reduce((memo, el) => {
var array = Array.isArray(el) ? flat(el) : [el];
return memo.concat(array);
}, []);
view raw createStore.js hosted with ❤ by GitHub

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay