terse-reduce

Monadic library to help with Typescripts --strictNullChecks

Usage no npm install needed!

<script type="module">
  import terseReduce from 'https://cdn.skypack.dev/terse-reduce';
</script>

README

Terse-Reduce

Javascript / Typescript state management with a special emphasis on:

  • easy integration with and incremental migration from traditional Redux reducers.
  • handling immutable updates through a mutable API powered by Immer
  • minimal boilerplate & maximum readability
  • strong typesafety for Typescript and strong autocompletion / refactoring support for Javascript (VSCode highly recommended)

Installation

yarn add terse-reduce

Examples

NOTE: All below code is written in Typescript. Javascript users can simply remove all Typescript interface and type declarations

Example 1: with sinlge 'root' reducer

import { createStore, Store } from 'redux';
import { manageRoot, initialize } from 'terse-reduce';
import axios from 'axios';

interface AppState { num: number, msg: string }
interface ApiError { status: number; message: string; }

const calculator = manageRoot<AppState>()
  .startWithState({ 
    num: 0,
    msg: ''
  })
  .processActions(take => ({
    ADD: take<number>()
      .update(_ => _.change.num += _.payload),
    SUBTRACT: take<number>()
      .update(_ => _.change.num -= _.payload),
    ADD_PI: take<number>()
      .update(_ => _.change.msg = `Calculating PI to ${_.payload} decimal places...`)
      .promise(_ => axios.get<number>(`${apiUrl}/calculate/pi?decimal-places=${_.payload}`))
      .update(_ => { _.change.num += _.promiseResult.data; _.change.msg = ''; })
      .catch<ApiError>(_ => _.promiseError.status === 400)
      .update(_ => _.change.notificationMessage = _.promiseRejection.message)
    })
  });

const { reducer, initialState } = initialize<AppState>(() => store.dispatch, calculator);
const store = createStore(reducer, initialState);  

calculator.ADD.dispatch(5);  

Example 2: with multiple 'branch' reducers

This is effecively the same as using 1 reducer per branch and then applying combineReducers()

interface Branch1State { propStr: string; propBool: boolean; }
interface Branch2State { propNum: number; propArr: number[]; }
interface AppState { branch1: Branch1State, branch2: Branch2State }

const branch1Manager = manageBranch<AppState, Branch1State>('branch1')
  .startWithState({
    propStr: '',
    propBool: false
  })
  .processActions(take => ({
    ACTION_ONE: take<string>()
      .update(_ => _.change.propStr = _.payload)
  }));

const branch2Manager = manageBranch<AppState, Branch2State>('branch2')
  .startWithState({
    propNum: 0,
    propArr: []
  })
  .processActions(take => ({
    ACTION_TWO: take<number>()
      .update(_ => _.change.propNum = _.payload)
  }));

const { reducer, initialState } = initialize<AppState>(() => store.dispatch, branch1Manager, branch2Manager);
const store: Store<AppState> = createStore(reducer, initialState);      

branch1Manager.ACTION_ONE.dispatch('Bake Bread');  

Example 3: Creating a re-usable chain of operations

Sometimes, one might want to create a re-usable chain of operations to be performed.
This is useful for highly repetitive logic, particularly when promises are involved.
For this we have takeRoot() and takeBranch() functions. Below is an example of takeRoot().

import { manageRoot, initialize, takeRoot } from "../src";
import { createStore, Store } from 'redux';
import axios from 'axios';

interface User { id: number, name: string, age: number }
interface Company { id: number, name: string }
interface AppState { showUiLoader: boolean, notificationMessage: string, users: User[], companies: Company[] }
interface ApiError { status: number; message: string; }

const postToApi = <P extends any[]>(endpoint: string, getArray: (s: AppState) => P) =>
  takeRoot<AppState, P>()
    .promise(_ => axios.post<P>(`/api/${endpoint}`, { body: _.payload }))
    .update(_ => getArray(_.change).push(_.promiseResult))
    .catch<ApiError>(_ => _.promiseError.status === 400)
    .update(_ => _.change.notificationMessage = _.promiseRejection.message);

const manageState = manageRoot<AppState>()
  .processActions(take => ({
    SAVE_USER: postToApi('/user', s => s.users),
    SAVE_COMPANY: postToApi('/company', s => s.users)
  }))