5.1.1 • Published 4 years ago

typestately v5.1.1

Weekly downloads
3
License
MIT
Repository
github
Last release
4 years ago

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.
  ...
5.1.1

4 years ago

5.1.0

4 years ago

5.0.0

4 years ago

4.1.0

4 years ago

4.0.5

5 years ago

4.0.4

5 years ago

4.0.3

5 years ago

4.0.2

5 years ago

4.0.1

5 years ago

4.0.0

5 years ago

3.3.0

5 years ago

3.2.0

5 years ago

3.1.0

5 years ago

3.0.0

5 years ago

2.2.3

5 years ago

2.2.2

5 years ago

2.2.1

5 years ago

2.2.0

5 years ago

2.1.1

5 years ago

2.1.0

5 years ago

2.0.3

6 years ago

2.0.2

6 years ago

2.0.1

6 years ago

2.0.0

6 years ago

1.2.1

6 years ago

1.2.0

6 years ago

1.1.0

6 years ago

1.0.3

6 years ago

1.0.2

6 years ago

1.0.1

6 years ago