Skip to content

API Overview

App: Class

The constructer function of App accepts an optional argument config: AppConfig.

const config: AppConfig = { middlewares, immutableRootState: false }
const app = new App(config)
Interface AppConfig
interface AppConfig {
  middlewares?: Redux.Middleware[]
  immutableRootState?: boolean
}
NameTypeDescriptionOptional
middlewaresstringAn array of Redux middleware.
Note: Reapex has redux-saga middleware by default.
Yes
immutableRootStatebooleanWhen it is true, reapex will create Immuable.Map as redux root state. The default is false which redux global state is a plain object.Yes

model: <T extends StateShape, S extends T | ImmutableRecord<T>>(namespace: string, initialState: S) => Model

Think of model as a slice of redux global state. Reapex internally combines different models via combineReducers. A model has an unique namespace and an initialState which is a plain object or Immutable Record.

const model = app.model('LoginForm', { username: '', password: '' })
Interface Model
interface Model {
  mutations: (mutationMap: P, subscriptions?: S) => [ActionCreators, ActionTypes]
  selectors: Selectors<T>
  effects: (effectMap: P, triggerMap?: S) => [ActionCreators, ActionTypes]
}

mutations: (mutationMap: P, subscriptions?: S) => [ActionCreators, ActionTypes]

The mutations describes how the model data will be updated. The mutations function accepts an object as the first parameter which key describe the operation and value is a function that describes the mutation input and output.

Note: once the initialValue is properly typed, you will get state strong-typed as well.

The model.mutations() returns a tuple: [actionCreators, actionTypes]. The actionCreators is an object which has the same keys as mutationMap and the values are strong-typed action creators.

The actionTypes is an object which has the same keys as the object passed into mutations() but values are strong-typed action types.

const [actionCreators, actionTypes] = model.mutations({
  setUserName: (username: string) => state => ({ ...state, username }),
  setPassword: (password: string) => state => ({ ...state, password }),
})

The generated actionCreators:

actionCreators = {
  setUserName: (username: string) => ({ type: 'LoginForm/setUserName', payload: [username] }),
  setPassword: (password: string) => ({ type: 'LoginForm/setPassword', payload: [password] }),
}

The second argument of mutations() function is used to subscribe to external actions. For example, actions of other model. It has the same interface as mutationMap but the difference is the keys are the actual action types.

const [actionCreators, actionTypes] = model.mutations({
  setUserName: (username: string) => state => ({ ...state, username }),
  setPassword: (password: string) => state => ({ ...state, password }),
}, {
  [managerActionTypes.reset]: () => state => ({ username: '', password: '' })
})

selectors: Selectors<T>

Reapex generates selectors for each individual field of the state.

const username = model.selectors.username(state)
const password = model.selectors.password(state)

The selectors can be used later with react-redux connect() function or with hooks

import { useSelector } from 'react-redux'

const username = useSelector(UserInfoModel.selectors.username)
const password = useSelector(UserInfoModel.selectors.password)

effects:(effectMap: P, triggers?: S) => [ActionCreators, ActionTypes]

The effects define application side effects, which are internally handled via redux-saga.

The model.effects() provides a simplified and structured way of creating sagas. For example, if we want to make an API call to validate the username when it changes, we can simply do:

model.effects({
  setUserName: {
    *takeEvery(action: ReturnType<typeof actionCreators.setUserName>) {
      const [username] = action.payload
      yield call(api.validate, username)
    }
  }
})

The above saga is run on every username change because it runs takeEvery effects. Apart from takeEvery effect, Reapex supports takeLatest, debounce, throttle effects as well. Please refer to: redux saga documentation for more details about different effects.

If you don't want to call the validation API every time username changed, because setUserName maybe called every time when the user types. Running debounce effect maybe more reasonable.

model.effects({
  setUserName: {
    *debounce(action: ReturnType<typeof actionCreators.setUserName>) {
      const [username] = action.payload
      yield call(api.validate, username)
    },
    ms: 2000,
  }
})

Note: debounce and throttle effect has an extra configure field: ms.

There are scenarios that one action doesn't mutate the state directly but only triggers side effects. For example, click login button doesn't update the state but just make an API call to authenticate the user.

The model.effects() function accepts a second argument which we call it triggers, we can define action creators which only trigger sagas.

const [effectActionCreators, effectActionTypes] = model.effects({
  setUserName: {
    *debounce(action: ReturnType<typeof actionCreators.setUserName>) {
      const [username] = action.payload
      yield call(api.validate, username)
    },
    ms: 2000,
  }
}, {
  submit: {
    *takeLatest() {
      const username = yield select(model.selectors.username)
      const password = yield select(model.selectors.password)
      yield call(api.login, username, password)
    }
  }
})

// Then in the component
<button onClick={() => dispatch(effectActionCreators.submit())}>Login</button>

The above example defines a submit action. It will call api.login when it's triggerred.


createStore: () => Redux.Store

Create Redux store which will be passed to <Provider>.

const store = app.createStore()
render(
  <Provider store={store}>
    <LoginForm />
  </Provider>,
  rootElement,
)

runSaga: (saga: Saga) => void

This is useful if you were adopting Reapex to an existing Redux application which uses redux-saga middleware. app.runSaga(saga) function allows to run saga directly without defining model and effects, for example, migrating existing sagas to use Reapex.


use: (logic: Reapex.Logic) => any

Applying reusable logic with app.use(logic).

A logic in Reapex is a reusable module which is sharable and can be published to NPM. For example, reapex-module-dataloader.


plugin: (plugin: Reapex.Plugin) => void

Extends Reapex with plugins. Reapex plugin is on a very early stage and only support limited features.

Check an example at: reapex-plugin-immer.


Logic: (app: App, ...args: any[]) => any

The Reapex Logic provides an unified and easy way of writing sharable Redux code. For example, we can wrap the LoginForm logic and provides it as a Reapex Plugin.

function loginModule(appInstance: App, namespace: string) {
  const model = appInstance.model('LoginForm', { username: '', password: '' })
  const [actionCreators] = model.mutations({
    setUserName: (username: string) => state => ({ ...state, username }),
    setPassword: (password: string) => state => ({ ...state, password }),
  })
  const [effectActionCreators] = model.effects(
    {
      setUserName: {
        *debounce(action: ReturnType<typeof actionCreators.setUserName>) {
          const [username] = action.payload
          yield call(console.log, username)
        },
        ms: 2000,
      },
    },
    {
      submit: {
        *takeLatest() {
          const username = yield select(model.selectors.username)
          const password = yield select(model.selectors.password)
          yield call(console.log, username, password)
        },
      },
    },
  )
  return {
    model,
    actionCreators,
    effectActionCreators,
  }
}

const { model, actionCreators, effectActionCreators} = app.use(loginModule, 'LoginForm')
export const LoginForm = () => {
  const username = useSelector(model.selectors.username)
  const password = useSelector(model.selectors.password)
  const dispatch = useDispatch()

  return (
    <>
      <div>
        User Name:
        <input type="text" value={username} onChange={e => dispatch(actionCreators.setUserName(e.target.value))} />
      </div>
      <div>
        Age:
        <input type="password" value={password} onChange={e => dispatch(actionCreators.setPassword(e.target.value))} />
      </div>
      <button onClick={() => dispatch(effectActionCreators.submit())}>Submit</button>
    </>
  )
}