📄
react-states
  • react-states API
  • Introduction
  • Our journey
  • The Mental Model
  • Adopting react-states
  • Reference implementations
  • Environment interface
  • Subscriptions
  • Features
  • Writing UI components
  • Explicit states
  • State transitions
  • Transition effects
  • Feature testing
  • Files and folders
  • Patterns
  • PR review guide
  • Devtools
Powered by GitBook
On this page
  • Exposing the environment
  • Implementing an environment effect
  • Environment variables

Was this helpful?

Environment interface

PreviousReference implementationsNextSubscriptions

Last updated 3 years ago

Was this helpful?

Building a React application with the react-states architecture requires you to think about your application as being mounted into an environment. The contract between your application and the environment is the Environment interface. This is a contract of specific application requirements. It is up to the environment to meet these requirements. In short, the application has absolutely no idea what environment it is running in.

The mindset of defining an Environment interface is to be very specific. What does the application actually care about? For example the application does not care about fetch, axios or any other library that fetches data. It does not even care about the urls you use to fetch that data. Actually, it does not even care about where the data is coming from, the application only cares about the data itself.

So let us examine this by example.

export interface Request {
  get<T>(url: string): Promise<T>
}

This is not a good interface as you expose to the application that it does an HTTP Get request and the application itself needs to provide the urls for where to fetch the data. A proper Environment interface would be:

export interface Api {
  getSandbox(id: string): Promise<SandboxDTO>
}

With getSandbox we are explicit about what the application wants from its environment. But an equally important point is that the application does not need to express any magical strings or typing related to grabbing data, it is already typed and contained where it belongs.

Exposing the environment

We expose the environment using a context provider. In the index.tsx at the root of the environment folder you can create a simple provider:

import React, { createContext, useContext } from "react
import { createEnvironment } from 'react-states'
import { EffectA } from './EffectA'

export interface Environment {
  effectA: EffectA
}

export const EnvironmentContext = createContext<Environment>({} as Environment)

export const useEnvironment = (): Environment => useContext(EnvironmentContext)

export {
  EnvironmentProvider: EnvironmentContext.Provider,
  useEnvironment
}

Implementing an environment effect

environment/
  index.tsx
  EffectA/
    test.ts
    browser.ts
    index.ts

The main file of an environment effects is EffectA/index.ts. This file exports the interface for the effect itself and other types related to the effect. It is important that this index file does not hold any implementations, as you do not want to accidentally import something you do not need in a certain environment. It also ensures that all types are imported directly from the effect as:

import { TypeA, TypeB } from 'environment/EffectA'

The actual implementations resides in a file representing the environment itself. For example browser.ts or test.ts. But this could be whatever other environments you want to use the features. React is environment agnostic, which means the feature could be native.ts or server.ts. You could even have a sandbox.ts environment where all the environment effects are hardcoded. An example of implementing a browser storage interface would be:

environment/Storage/browser.ts
import { Storage } from './'

export const createStorage = ():Storage => ({
  set(key, value) {
    localStorage.setItem(key, JSON.stringify(value))
  }
  get(key) {
    return JSON.parse(localStorage.getItem(key) || 'undefined')
  }
})

You always define environment effects as factories, as this allows to pass in options when instantiating the effect. This could be environment variables and the likes.

The application would consume this effect by:

src/main.tsx
import * as React from 'react'
import { render } from 'react-dom'
import { createStorage } from '../environment/storage/browser'
import { EnvironmentProvider } from '../environment'

render(
  <EnvironmentProvider value={{
    storage: createStorage()
  }}>
    <App />
  </EnvironmentProvider>
)

The same effect can be implemented for the test environment doing:

environment/Storage/test.ts
import { Storage } from './'

export const createStorage = (): Storage => ({
  set: jest.fn(),
  get: jest.fn()
})

Best practices

Environment variables

The features never expresses environment variables in their implementation. You rather pass environment variables when creating an effect. For example:

src/main.tsx
import * as React from 'react'
import { render } from 'react-dom'
import { createApi } from '../environment/api/browser'
import { EnvironmentProvider } from '../environment'

render(
  <EnvironmentProvider value={{
    api: createApi(process.env.API_URL)
  }}>
    <App />
  </EnvironmentProvider>
)

This ensures two things:

  1. Environment effects are defined in isolation, they should not make assumptions about the names of environment variables which is out of control of the environment effect. It is up to the application that consumes all the environment effects to pass explicit options to the effects, which could be an environment variable (as seen in the example above)

  2. You have a single point of understanding how these environment effects are configured, including what environment variables are actually being consumed

Note that returning promises is not encouraged as React can see their resolvements as possible memory leaks. Read more about to see how to implement environment effects in the react-states architecture

subscriptions