Skip to content

Recomposed approach of using redux with TypeScript

Notifications You must be signed in to change notification settings

hmuralt/typestately

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

typestately

Recomposed approach of using redux with TypeScript. An idea showing how you can deal with state management using redux.

build status npm version

Some goals

  • Reduce needed type annotation by making use of type inference
  • Reduce boilerplate code
  • Encapsulate state details/concerns (e.g. key used for a reducer in the stores state object) in one place
  • Easy way to plug new parts of global state in and out
  • Support code-splitting
  • Support multiple stores

Examples/HowTo

A complete example can be found here: https://github.com/hmuralt/typestately-example

It shows the usage with state handlers implemented as classes and an alternative usage with plain objects & functions.

Store

This is an example of how stores could be setup and registered.

StoreContexts.ts

import { setupMainStoreContext } from "./StoreContextSetups";

const storeContexts = {
  Main: setupMainStoreContext()
};

export default storeContexts;

StoreContextSetups.ts

import { createStore } from "redux";
import { createStoreContext } from "typestately";

export function setupMainStoreContext() {
  const store = createStore((state) => state || {});

  return createStoreContext(store, {});
}

Counter example with state handler classes

State

CounterState.ts

export default interface State {
  value: number;
  clicked: Date;
}

export const defaultState: State = {
  value: 0,
  clicked: new Date()
};

Actions

CounterActions.ts

export enum ActionType {
  Increment = "INCREMENT",
  Decrement = "DECREMENT"
}

export interface ChangeAction extends Action<ActionType> {
  type: ActionType;
  clicked: Date;
}

State handler

CounterStateHandler.ts

class CounterStateHandler extends StateHandler<State, ActionType> {
  @StateHandler.nested
  public readonly loaderStateHandler: LoaderStateHandler;

  constructor(loaderStateHandler: LoaderStateHandler) {
    super("counter", defaultState);

    this.loaderStateHandler = loaderStateHandler;
  }

  public increment(clicked: Date) {
    this.dispatch<ChangeAction>({
      type: ActionType.Increment,
      clicked
    });
  }

  public decrement(clicked: Date) {
    this.dispatch<ChangeAction>({
      type: ActionType.Decrement,
      clicked
    });
  }

  public incrementAsync(clicked: Date) {
    this.loaderStateHandler.setStatus(Status.Updating);
    window.setTimeout(() => {
      this.increment(clicked);
      this.loaderStateHandler.setStatus(Status.Done);
    }, 2000);
  }

  @StateHandler.reducer<State, ActionType>(ActionType.Increment)
  protected reduceIncrement(state: State, action: ChangeAction) {
    return {
      value: state.value + 1,
      clicked: action.clicked
    };
  }

  @StateHandler.reducer<State, ActionType>(ActionType.Decrement)
  protected reduceDecrement(state: State, action: ChangeAction) {
    return {
      value: state.value - 1,
      clicked: action.clicked
    };
  }
}
// Ideally managed by IOC container...
const counterStateHandler = new CounterStateHandler(new LoaderStateHandler());

export default counterStateHandler;

Component

Counter.tsx

export interface Props {
  value: number;
  clicked: Date;
  onIncrement: (clicked: Date) => void;
  onIncrementAsync: (clicked: Date) => void;
  onDecrement: (clicked: Date) => void;
}

export default class Counter extends React.Component<Props> {
  constructor(props: Props) {
    super(props);

    this.increment = this.increment.bind(this);
    this.incrementAsync = this.incrementAsync.bind(this);
    this.decrement = this.decrement.bind(this);
  }

  public render() {
    return (
      <div>
        <p>
          Value: {this.props.value} (clicked: {this.props.clicked.toLocaleString()})
        </p>
        <p>
          <button onClick={this.decrement}>-</button>
          <button onClick={this.increment}>+</button>
        </p>
        <p>
          <button onClick={this.incrementAsync}>+ (async)</button>
        </p>
      </div>
    );
  }

  private increment() {
    this.props.onIncrement(new Date());
  }

  private incrementAsync() {
    this.props.onIncrementAsync(new Date());
  }

  private decrement() {
    this.props.onDecrement(new Date());
  }
}

Container

Counter.ts

counterStateHandler.attachTo(storeContexts.Main.hub);

export default withStateToProps(
  counterStateHandler,
  (counterState): Props => {
    return {
      value: counterState.value,
      clicked: counterState.clicked,
      onIncrement: (clicked: Date) => counterStateHandler.increment(clicked),
      onIncrementAsync: (clicked: Date) => counterStateHandler.incrementAsync(clicked),
      onDecrement: (clicked: Date) => counterStateHandler.decrement(clicked)
    };
  }
)(Counter);

Counter example with state handler functions (alternative to classes)

State

export default interface CounterState {
  value: number;
  clicked: Date;
}

export const defaultCounterState: CounterState = {
  value: 0,
  clicked: new Date()
};

Actions

CounterActions.ts

export enum ActionType {
  Increment = "INCREMENT",
  Decrement = "DECREMENT"
}

export interface ChangeAction extends Action<ActionType> {
  type: ActionType;
  clicked: Date;
}

Reducer

function increment(state: CounterState, action: ChangeAction) {
  return {
    value: state.value + 1,
    clicked: action.clicked
  };
}

function decrement(state: CounterState, action: ChangeAction) {
  return {
    value: state.value - 1,
    clicked: action.clicked
  };
}

const counterReducer = createExtensibleReducer<CounterState, ActionType>()
  .handling(ActionType.Increment, increment)
  .handling(ActionType.Decrement, decrement);

export default counterReducer;

State handler

const counterStateDefinition = defineState(defaultCounterState)
  .makeStorableUsingKey("counter")
  .setReducer(() => counterReducer)
  .setActionDispatchers({
    increment(dispatch: Dispatch<ActionType>, clicked: Date) {
      dispatch<ChangeAction>({
        type: ActionType.Increment,
        clicked
      });
    },
    decrement(dispatch: Dispatch<ActionType>, clicked: Date) {
      dispatch<ChangeAction>({
        type: ActionType.Decrement,
        clicked
      });
    }
  });

export function createCounterStateHandler(hub: Hub) {
  const counterStateHandler = counterStateDefinition.createStateHandler(hub);
  const loaderStateHandler = createLoaderStateHandler(hub, counterStateHandler.contextId);
  const extensions = {
    incrementAsync(clicked: Date) {
      loaderStateHandler.setStatus(Status.Updating);

      window.setTimeout(() => {
        counterStateHandler.increment(clicked);

        loaderStateHandler.setStatus(Status.Done);
      }, 2000);
    }
  };

  return Object.assign(counterStateHandler, extensions, {
    loaderStateProvider: withStateProvider(loaderStateHandler)({})
  });
}

Container

const CounterContainer: React.FC = () => {
  const counterStateHandler = React.useMemo(() => createCounterStateHandler(storeContexts.FunctionsExample.hub), []);
  const counterState = useStateProvider(counterStateHandler);

  return (
    <Counter
      value={counterState.value}
      clicked={counterState.clicked}
      onIncrement={counterStateHandler.increment}
      onIncrementAsync={counterStateHandler.incrementAsync}
      onDecrement={counterStateHandler.decrement}
    />
  );
};

State without using redux

You can also create a standalone state handler which isn't attached to the redux store and has it's own standalone state.

interface CounterState {
  value: number;
  clicked: Date;
}

const defaultCounterState: CounterState = {
  value: 0,
  clicked: new Date()
};

function increment(state: CounterState, clicked: Date) {
  return {
    value: state.value + 1,
    clicked
  };
}

function decrement(state: CounterState, clicked: Date) {
  return {
    value: state.value - 1,
    clicked
  };
}

const counterStateDefinition = defineState(defaultCounterState, { increment, decrement });

const counterStateHandler = counterStateDefinition.createStandaloneStateHandler();
counterStateHandler.increment(new Date());

And, you can extend the existing an state definition with Redux if needed.

counterStateDefinition
  .makeStorableUsingKey("counter")
  .setReducer((stateOperations) => ...) // stateOperations = { increment, decrement } object from defineState call.
  ...

About

Recomposed approach of using redux with TypeScript

Resources

Stars

Watchers

Forks

Packages

No packages published