Skip to content

Instantly share code, notes, and snippets.

@cmlarsen
Last active July 6, 2020 20:22
Show Gist options
  • Save cmlarsen/a66f0030164d71b524565ef76a3bfc29 to your computer and use it in GitHub Desktop.
Save cmlarsen/a66f0030164d71b524565ef76a3bfc29 to your computer and use it in GitHub Desktop.
A bare bones shared state system using a pub/sub pattern and Reacts underlying useState hooks. See example in this Repl: https://repl.it/@cmlarsen/SharedState
import React, {useState, useEffect, useRef, useCallback, useMemo} from 'react'
import * as ReactDOM from 'react-dom'
import {get, set} from 'lodash'
//The guts of the pub/sub system
const sharedState = {
//Holds the state, this is mutable, but could easily be locked down
state: {},
//holds the watchers.
watchers:{},
//setup a new watcher for a "bit of state"
watch (bitOfState, callback) {
if (!this.watchers[bitOfState]) {
this.watchers[bitOfState] = new Set();
}
//add the callback to the set of watchers
this.watchers[bitOfState].add(callback)
//fire the initial value
callback(get(this.state, bitOfState))
},
//remote a watcher on a "bit of state"
unwatch (bitOfState, callback) {
this.watchers[bitOfState].delete(callback)
},
//set a value for a "bit of state"
set(bitOfState, value) {
//don't update if no change
if (get(this.state, bitOfState) === value) return
//update the state object
set(this.state, bitOfState, value)
//fan out the changes to all the watchers
for (let callback of this.watchers[bitOfState]) {
callback(get(this.state,bitOfState))
}
},
//Initialize the state
init(initialState){
this.state = initialState
}
}
//The main intercace hook
function useSharedState(bitOfState) {
const [value, setValue] = useState(get(sharedState.state,bitOfState));
useEffect(() => {
const watcher = _value => {
setValue(_value)
}
sharedState.watch(bitOfState, watcher)
return () => {
sharedState.unwatch(bitOfState, watcher)
}
}, [])
const setter = useCallback((input) => {
let value;
if(typeof input === 'function'){
value = input(get(this.state,bitOfState))
} else {
value = input
}
sharedState.set(bitOfState, input)
},[])
return [value, setter]
}
function ExampleComponent1(){
const [time, setTime] = useSharedState('time')
const [counter, setCounter] = useSharedState('nested.kinda.deep')
const [weather, setWeather] = useSharedState('weather')
useEffect(()=>{
setInterval(()=>{
//setting state based on current value
setCounter(_value=>_value+1)
},1000)
},[])
useEffect(()=>{
fetch('https://api.weather.gov/gridpoints/TOP/31,80/forecast').then(res=>res.json()).then(result=>{
setWeather(result.properties.periods[0].shortForecast)
})
},[]);
return <div>Component 1 <br/> {counter} <br/> { time.toISOString()} <br/> {weather} </div>
}
function ExampleComponent2(){
const [time, setTime] = useSharedState('time')
const [counter] = useSharedState('nested.kinda.deep')
const [weather] = useSharedState('weather')
useEffect(()=>{
setInterval(()=>{
setTime(new Date())
},1000)
},[])
return <div>Component 2 <br/> {counter} <br/> { time.toISOString()} <br/> {weather} </div>
}
function App(){
sharedState.init({
time:new Date(),
weather:undefined,
nested:{
kinda:{
deep:0
}
}})
return <div className="App">
<ExampleComponent1/>
<ExampleComponent2/>
</div>
}
ReactDOM.render(<App />, document.getElementById('root'));
@cmlarsen
Copy link
Author

cmlarsen commented Jul 6, 2020

There is a lot of room for improvement here and some foot guns lying around. But as a direction, I am very curious.

@cmlarsen
Copy link
Author

cmlarsen commented Jul 6, 2020

It would be nice to allow the setter to be async. This would likely require wrapping the the result of the setter function in a promise

@cmlarsen
Copy link
Author

cmlarsen commented Jul 6, 2020

Could have problems if you subscribe to a child and then update it's parent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment