javascript

Observable – удобный state-manager

  • понедельник, 25 ноября 2024 г. в 00:00:05
https://habr.com/ru/articles/860820/

Я вас понимаю. Да – еще один. Но давайте посмотрим, вдруг правда?

Давайте определимся с тем, что такое удобно. Конечно, у нас разные представления об удобстве, поэтому я опишу свои с примерами из api react:

  • Отсутствие boilerplate.

// неудобно для меня
const [state, setState] = useState({ count: 0 })

// было бы удобнее
const state = useState({ count: 0 })
  • Отсутствие snakepit. Термин придумал сам. Под ним я понимаю необходимость совершать и/или оборачивать операции изменения/присвоения в дополнительный код:

// неудобно для меня
const [state, setState] = useState({ count: 0 })
const setCount = (value) => setState(prev => ({ count: prev.count + value }))

// было бы удобнее
const state = useState({ count: 0 })
const setCount = (value) => state.count = value
  • Ограничения. Например – нельзя вызвать хук внутри условия. Мне неудобно.

Делают ли эти примеры библиотеку react плохой? Разумеется нет, но без этих ограничений – было бы удобнее.

С этой точки зрения, Observable удобен тем, что не накладывает ограничений. Никаких.

Базовый пример

Извините. Дальше будут нормальные примеры, но без каунтера нельзя. Надо соблюдать традиции.

import { Observable, observer } from 'kr-observable'

class CounterState extends Observable {
  count = 0

  increase() { 
    ++this.count
  }

  decrease() {
    --this.count
  }
}

const state = new CounterState()

Это наш стейт. Теперь давайте его используем

// можно так
const Counter = observer(() => {
  return (
    <div>
      <button onClick={state.decrease}>
        Decrease
      </button>
      <div>Count: {state.count}</div>  
      <button onClick={state.increase}>
        Increase
      </button>
    </div>
  )
})
// или так
const Counter = observer(() => {
  return (
    <div>
      <button onClick={() => --state.count}>
        Decrease
      </button>
      <div>Count: {state.count}</div>  
      <button onClick={() => ++state.count}>
        Increase
      </button>
    </div>
  )
})
// или вот так
const increase = () => state.increase()
const decrease = () => state.decrease()

const Counter = observer(() => {
  return (
    <div>
      <button onClick={increase}>
        Decrease
      </button>
      <div>Count: {state.count}</div>  
      <button onClick={decrease}>
        Increase
      </button>
    </div>
  )
})

Иными словами работу с Observable можно описать так: Напишите рабочий код на JavaScript. Если хотите связать его с React:

  1. Оберните компоненты в hoc observer

  2. Добавьте нужным классам extends Observable и/или примените к объектам декоратор makeObservable

Надеюсь я вас убедил, что использовать Observable удобно 😉

Дополнительные преимущества

  • Малый размер – 2.7 kb (Gzipped, non minified);

  • Производительная;

  • Framework-agnostic. Из коробки идет с hoc-ом для React, как самой популярной библиотеки, но может работать с любой другой, или без – vanilla, node.js.

Я обещал пример сложнее каунтера. Пожалуйста:

class State extends Observable {
  results = []
  text = ''
  loading = false
  
  // All methods are automatically bounded, 
  // so you can safely use them as listeners
  setText(event: Event) {
    this.text = event.target.value
  }
  
  async search() {
    try {
      this.loading = true
      const response = await fetch(`/api/search?=${this.text}`)
      this.results = await response.json()
    } catch(e) {
      console.warn(e)
    } finally {
      this.loading = false
    }
  }
  
  reset() {
    this.results = []
  }
}

const state = new State()

const Results = observer(function results() {
  // Will re-render only if the results change
  return (
    <div>
      {state.results.map(result => <div key={result}>{result}</div>)}
    </div>
  )
})

const Component = observer(function component() {
  // Will re-render only if the text or loading change
  return (
    <div>
      <input 
        placeholder="Text..." 
        onChange={state.setText}
        disabled={state.loading}
        value={state.text}
      />
      <button 
        onClick={state.search}
        disabled={state.loading}
      >
        Submit
      </button>
      
      <button onClick={state.reset}> 
        Reset
      </button>
      <Results />
    </div>
  )
})

Разумеется, не имеет значения каким образом state оказался внутри Component . При передачи через props поведение не изменится.

Пример с реактивностью. Тут используется autorun , переданная в autorun функция будет вызываться каждый раз, когда в корзину попадает новый товар.

import { Observable, autorun } from 'kr-observable'

class Cart extends Observable {
  products = []
}

const cart = new Cart()

let interval

autorun(() => {
  const total = catrt.products.reduce((sum, product) => sum + product.price, 0)
  if (total > 5000) {
    clearInterval(interval)
    console.log('Amount is more than 5000')
  }
})

interval = setInterval(() => {
  cart.products.push({ price: 1000 })
}, 1000)
// "Amount is more than 5000" (after 5s)

Демо на CodeSandbox

Полная документация на Github или в Npm.

Спасибо что дочитали.