BeTomorrow/micro-observables

Individual observables vs object

Closed this issue · 2 comments

Hi there,

I'm really enjoying the simplicity of this library and pattern. It reminds me of mobx but much simpler/lighter.

I'm wondering if you have an example in the wild using this library however as I'm trying to decide between the right patterns that offer nice syntax sugar, readability, succinctness and performance.

One question is, in a TodoService (which I prefer to call TodoStore), I would usually have a number of fields inside of a store, and I'm trying to decide if I should make a single observable state object, or break it into multiple observable values.

class SomeStore {
  state = observable({
    enabled: false,
    name: '',
    age: 30
  })
}

or.. separate..

class SomeStore {
  enabled = observable<boolean>(false)
  name = observable<string>('')
  age = observable<number>(30)
}

I will admit, I prefer the second approach with individual observables, but, I can't manage to come up with a new useObservable or a new useStore hook pattern to make it easy to consume the entirety of the store without having to relist each individual observable.

the other part to this pattern is considering serialization which is useful for cases when an app needs SSR.

thank you

I personally use the second approach with individual observables, as it makes it easier to update a single state variable, without having to rely on the spread operator. This way, I also don't have to use a "selector" in my React components to get the state variable that I'm interested in.

In the latest version of micro-observables, you can also use the not-yet-documented Observable.from() function to easily combine several observables into a single one, which could make it easier for you to retrieve several observables at once.

neat, thanks for the response.. btw, here is a nice pattern we've built into our application

import React from 'react'
import { useContext, useState, useEffect, useMemo } from 'react'
import { Observable, Unsubscriber } from 'micro-observables'

import { AppStore } from './AppStore'
import { AuthStore } from './AuthStore'
import { WalletStore } from './WalletStore'
import { RouterStore, syncHistoryWithStore } from './RouterStore'
import { RemoteControlStore } from './RemoteControlStore'
import { ModalStore } from './ModalStore'
import { TokenStore } from './TokenStore'
import { ContactStore } from './ContactStore'

export { AppStore, RouterStore, ModalStore, AuthStore, WalletStore, TokenStore, ContactStore }

export class RootStore {
  app = new AppStore(this)
  router = new RouterStore(this)
  modal = new ModalStore(this)

  auth = new AuthStore(this)
  contact = new ContactStore(this)
  token = new TokenStore(this)
  wallet = new WalletStore(this)
  remoteControl = new RemoteControlStore(this)
}

export const createStore = () => new RootStore()

export const StoreContext = React.createContext<RootStore | null>(null)

export const StoreProvider = ({ store, children }: { store: RootStore; children: React.ReactNode }) => {
  return <StoreContext.Provider value={store}>{children}</StoreContext.Provider>
}

export function useRootStore(): RootStore {
  const store = useContext(StoreContext)
  if (!store) {
    throw new Error('store cannot be null! check your <StoreProvider ...>')
  }
  return store
}

// useStore hook will access the rootStore instance from the application context and return
// the specific sub-store. You can set the `observe` parameter to observe changes for all fields,
// specific fields or none of the fields, so that React components that use the `useStore` are
// able to automatically re-render from state changes.
//
// observe: when the argument is undefined, it will watch all observable fields of the store. In other words
// if you don't specify the observe argument's functionality, the default is to observe the entire store.
//
// observe: when null, it won't observe any of the fields in the store.
//
// observe: by passing a function that returns an array of observable fields, this hook will only watch
// and re-render whenever one of those specific observables changes. This is handy as a performance optimization.
export function useStore<T>(storeKey: keyof RootStore, observe?: null | ((store: T) => Observable<any>[])): T {
  const store = useRootStore()[storeKey] as any

  if (observe === null) {
    return store as T
  }

  let observables: Observable<any>[]

  if (observe === undefined) {
    observables = useMemo<Observable<any>[]>(() => {
      let v: Observable<any>[] = []
      const keys = Object.keys(store)
      for (let i = 0; i < keys.length; i++) {
        if (store[keys[i]] instanceof Observable) {
          v.push(store[keys[i]])
        }
      }
      return v
    }, [store])
  } else {
    observables = observe(store)
  }

  const [, forceRender] = useState({})

  useEffect(() => {
    const unsubscribers: Unsubscriber[] = []
    for (let i = 0; i < observables.length; i++) {
      unsubscribers.push(
        observables[i].onChange(() => {
          forceRender({})
        })
      )
    }
    return () => {
      for (let i = 0; i < unsubscribers.length; i++) {
        unsubscribers[i]()
      }
    }
  }, observables)

  return store as T
}

export { observable, Observable } from 'micro-observables'
export { syncHistoryWithStore }

specifically, see useStore() hook