react-model-store

The simple state management library for React

Usage no npm install needed!

<script type="module">
  import reactModelStore from 'https://cdn.skypack.dev/react-model-store';
</script>

README

React Model Store

npm version Build Status Coverage Status License: MIT

The simple state management library for React.

This library provides model-based state management with Hooks and Context API of React.

Install

npm install react-model-store

or

yarn add react-model-store

Requirements

  • React 16.8.0 or newer

Examples for Typescript

Counter Example (single component pattern)

import React from 'react';
import ReactDOM from 'react-dom';
import { Model, useModel } from 'react-model-store';

class CounterModel extends Model {
  count: number = this.state(0);

  // Synchronous
  increment = () => this.count++;

  // Asynchronous
  decrement = () => setTimeout(() => this.count--, 1000);
}

const Counter = () => {
  const { count, increment, decrement } = useModel(CounterModel);
  return (
    <div>
      <p>Count: {count}</p>
      <div>
        <button onClick={increment}>Increment</button>
        <button onClick={decrement}>Decrement</button>
      </div>
    </div>
  );
};

ReactDOM.render(<Counter />, document.getElementById('root'));

Todo Example (model provider pattern)

import React from 'react';
import ReactDOM from 'react-dom';
import { Model, createStore, useModel } from 'react-model-store';

interface Todo {
  key: number;
  text: string;
}

class ControlModel extends Model {
  textInput = this.ref<HTMLInputElement>();
  onAddClick = this.event();
  onKeyPress = this.event<React.KeyboardEvent<HTMLInputElement>>();

  get text(): string {
    return this.textInput.current!.value;
  }

  refresh(): void {
    this.textInput.current!.value = '';
    this.textInput.current!.focus();
  }
}

class LogicModel extends Model {
  private control: ControlModel;
  lastKey: number = this.state(0);
  todos: Todo[] = this.state([]);

  constructor(control: ControlModel) {
    super();
    this.control = control;

    this.addListener(this.control.onAddClick, () => {
      this.add();
    });
    this.addListener(
      this.control.onKeyPress,
      (e: React.KeyboardEvent<HTMLInputElement>) => {
        if (e.key === 'Enter') {
          this.add();
        }
      }
    );
  }

  add(): void {
    if (this.control.text) {
      this.todos.push({
        key: ++this.lastKey,
        text: this.control.text,
      });
      this.control.refresh();
    }
  }

  remove(key: number): void {
    this.todos = this.todos.filter(todo => todo.key !== key);
  }
}

class RootModel {
  control = new ControlModel();
  logic = new LogicModel(this.control);
}

class TodoModel extends Model {
  todo: Todo;
  onRemoveClick: () => void;

  constructor(todo: Todo) {
    super();
    this.todo = todo;
    const { logic } = this.model(RootModelStore);
    this.onRemoveClick = logic.remove.bind(logic, todo.key);
  }
}

const RootModelStore = createStore(RootModel);

const ControlPanel = () => {
  const {
    control: { textInput, onAddClick, onKeyPress },
  } = useModel(RootModelStore);
  return (
    <div>
      <input type='text' ref={textInput} onKeyPress={onKeyPress} />
      <button onClick={onAddClick}>Add</button>
    </div>
  );
};

const TodoItem = (props: { todo: Todo }) => {
  const {
    todo: { text },
    onRemoveClick,
  } = useModel(TodoModel, props.todo);
  return (
    <li>
      <button onClick={onRemoveClick}>Remove</button>
      <span>{text}</span>
    </li>
  );
};

ReactDOM.render(  
  <RootModelStore.Provider>
    <div>
      <ControlPanel />
      <ul>
        <RootModelStore.Consumer>
          {({ logic: { todos } }) =>
            todos.map(todo => (
              <li>
                <TodoItem key={todo.key} todo={todo} />
              </li>
            ))
          }
        </RootModelStore.Consumer>
      </ul>
    </div>
  </RootModelStore.Provider>,
  document.getElementById('root')
);

Timer Example (high frequency re-render pattern)

import React from 'react';
import ReactDOM from 'react-dom';
import { Model, createStore, useModel } from 'react-model-store';

class RootModel extends Model {
  // RootModelStore.Provider component is re-rendered when this state is changed.
  running = this.state(false);

  resetButton = this.ref<HTMLButtonElement>();

  onReset = this.event();

  onToggle = this.event(() => {
    this.running = !this.running;
    this.resetButton.current!.disabled = this.running;
  });

  get toggleText(): string {
    return this.running ? 'Stop' : 'Start';
  }
}

const RootModelStore = createStore(RootModel);

class HighFrequencyTimerModel extends Model {
  root = this.model(RootModelStore); // use RootModel

  // HighFrequencyTimer component is re-rendered when this state is changed.
  time = this.state(0);
  started: number = 0;
  stored: number = 0;

  constructor() {
    super();
    this.addListener(this.root.onToggle, this.toggle);
    this.addListener(this.root.onReset, this.reset);
  }

  update(): void {
    this.time = this.stored + new Date().getTime() - this.started;
  }

  run = () => {
    if (this.root.running) {
      this.update();
      setTimeout(this.run, 50);
    }
  };

  toggle = () => {
    if (this.root.running) {
      this.started = new Date().getTime();
      this.run();
    } else {
      this.update();
      this.stored = this.time;
    }
  };

  reset = () => {
    this.stored = 0;
    this.time = 0;
  };
}

const HighFrequencyTimer = () => {
  const { time } = useModel(HighFrequencyTimerModel);
  return <span>{(time / 1000).toFixed(2)}</span>;
};

const Controller = () => {
  const { onReset, onToggle, toggleText, resetButton } = useModel(
    RootModelStore
  );
  return (
    <div>
      <button onClick={onToggle}>{toggleText}</button>
      <button onClick={onReset} ref={resetButton}>
        Reset
      </button>
    </div>
  );
};

ReactDOM.render(
  <RootModelStore.Provider>
    <div>
      <div>
        {/*
         * HighFrequencyTimer component is re-rendered frequently.
         * But that re-rendering doesn't cause re-rendering of the provider.
         */}
        <HighFrequencyTimer />
      </div>
      <Controller />
    </div>
  </RootModelStore.Provider>,
  document.getElementById('root')
);