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?