Subscriptions

When you implement the environment interface it is encouraged to use actions to signal resolved requests.

In React the term action is conflated in the sense that it represents both an action, like SIGN_IN, and events, like LOAD_DATA_SUCCESS. We decided to rather embrace this conflated term instead of trying to shift a very established mindset. This ensures consistent typing and naming things is hard enough, forcing events and past tense naming is unrealistic.

An example of this would be.

import { Subscription } from 'react-states'

type ApiAction =
  | {
    type: 'API:FETCH_SANDBOX_SUCCESS'
    id: string
    sandbox: SandboxDTO
  }
  | {
    type: 'API:FETCH_SANDBOX_ERROR'
    id: string
    error: string
  }

export type Api = {
  subscription: Subscription<ApiAction>
  /**
  * @fires API:FETCH_SANDBOX_SUCCESS
  * @fires API:FETCH_SANDBOX_ERROR
  */
  fetchSandbox(id: string): void
}

Now we are not returning anything, but calling fetchSandbox will result in one of two actions. Doing this does not only comply better with Reacts behaviour, but gives several other benefits:

  • Features does not have to translate the result of the Promise into an action, all actions from the subscription is passed into the feature to possibly be handled

  • Environment effects can emit actions synchronously and asynchronously

  • Environment effects can emit multiple actions. This can be related to caching, lazy updating and/or automatically subscribing to the data

  • You get typed errors

  • Tests no longer needs to run asynchronously as the responses can be triggered as synchronous actions

There are many opportunities to optimize how effects deals with over fetching, caching etc.

Prevent unnecessary requests

Only do a single fetch for any item requested at any time.

import { createSubscription } from 'react-states'
import { EnvironmentEffect, EnvironmentEffectAction } from './'

export const createEnvironmentEffect = (): EnvironmentEffect => {
  const subscription = createSubscription<EnvironmentEffectAction>()
  const pendingReads: { [id: string]: Promise<any> } = {}
  
  return {
    subscription,
    read(id: string) {
      pendingReads[id] = pendingReads[id] || fetch(id)
        .then((data) => {
          subscription.emit({
            type: 'READ_SUCCESS',
            id,
            data
          }) 
        }) 
        .catch((error) => {
          subscription.emit({
            type: 'READ_ERROR',
            id,
            error: error.message
          })         
        })
        .finally(() => {
          // Unset for any future reads
          delete pendingReads[id]
        })
    }
  }
}

Caching reads

You can keep the result to optimistically return the cache, while lazily loading the latest version.

import { createSubscription } from 'react-states'
import { EnvironmentEffect, EnvironmentEffectAction } from './'

export const createEnvironmentEffect = (): EnvironmentEffect => {
  const subscription = createSubscription<EnvironmentEffectAction>()
  const pendingReads: { [id: string]: Promise<any> } = {}
  const cachedReads: { [id: string]: any } = {}
  
  return {
    subscription,
    read(id: string) {
      const isCached = id in cachedReads
      
      if (isCached) {
        // We can trigger synchronous results
        // from the cache
        subscription.emit({
          type: 'READ_SUCCESS',
          id,
          data
        }) 
      }
      
      // We keep our logic for unnecessary reads
      pendingReads[id] = pendingReads[id] || fetch(id)
        .the((data) => {
          // We cache it only when successful
          cachedReads[id] = data
          
          // We emit different events based
          // on it being the initial read or
          // a lazy read
          subscription.emit({
            type: isCached ? 'READ_UPDATE' : 'READ_SUCCESS',
            id,
            data
          }) 
        }) 
        .catch((error) => {
          subscription.emit({
            type: 'READ_ERROR',
            id,
            error: error.message
          })         
        })
        .finally(() => {
          delete pendingReads[id]
        })
    }
  }
}

Read subscriptions

If you are able to subscribe to the data you read, that can be integrated with any read.

import { createSubscription } from 'react-states'
import { EnvironmentEffect, EnvironmentEffectAction } from './'

export const createEnvironmentEffect = (): EnvironmentEffect => {
  const subscription = createSubscription<EnvironmentEffectAction>()
  const pendingReads: { [id: string]: Promise<any> } = {}
  const cachedReads: { [id: string]: any } = {}
  
  return {
    subscription,
    read(id: string) {
      const isCached = id in cachedReads
      
      if (isCached) {
        subscription.emit({
          type: 'READ_SUCCESS',
          id,
          data
        })
      // We only read when there is no cached data, as
      // the subscription created will keep the cache
      // updated
      } else {
        pendingReads[id] = pendingReads[id] || fetch(id)
          .then((data) => {
            cachedReads[id] = data
            
            subscription.emit({
              type: 'READ_SUCCESS',
              id,
              data
            })
            
            // We subscribe when cache is ready
            // with initial data, where subscribe
            // is some 3rd party API to subscribe
            // to data
            subscribe(id, (updatedData) => {
              cachedReads[id] = updatedData
              subscription.emit({
                type: 'READ_UPDATE',
                id,
                data: updatedData
              })              
            })
          }) 
          .catch((error) => {
            this.events.emit({
              type: 'READ_ERROR',
              id,
              error: error.message
            })         
          })
          .finally(() => {
            delete pendingReads[id]
          })
      }
    }
  }
}

Last updated

Was this helpful?