Observable – удобный state-manager
- понедельник, 25 ноября 2024 г. в 00:00:05
Я вас понимаю. Да – еще один. Но давайте посмотрим, вдруг правда?
Давайте определимся с тем, что такое удобно. Конечно, у нас разные представления об удобстве, поэтому я опишу свои с примерами из 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:
Оберните компоненты в hoc observer
Добавьте нужным классам 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.
Спасибо что дочитали.