200 байт для управления состоянием React-компонентов
- React-хуки: это все, что нужно для управления состоянием.
- ~200 байт, min+gz.
- Знакомый API: просто пользуйтесь React, как обычно.
- Минимальный API: хватит пяти минут, чтобы разобраться.
- Написан на TypeScript, чтобы обеспечить автоматический вывод типов.
Главный вопрос: чем этот пакет лучше, чем Redux? Ну...
- Он меньше. Он в 40 раз меньше.
- Он быстрее. Изолируйте проблемы производительности на уровне компонентов.
- Он проще в изучении. Вам в любом случае нужно уметь пользоваться React-хуками и контекстом, они классные.
- Он проще в интеграции. Подключайте по одному компоненту за раз, не ломая совместимости с другими React-библиотеками.
- Он проще в тестировании. Тестировать отдельно редьюсеры — напрасная трата времени, упростите тестирование самих React-компонентов.
- Он проще с точки зрения типизации. Написан так, чтобы максимально задействовать выведение типов.
- Он минималистичный. Это просто React.
Пример кода
import React, { useState } from "react"
import { createContainer } from "unstated-next"
import { render } from "react-dom"
function useCounter() {
let [count, setCount] = useState(0)
let decrement = () => setCount(count - 1)
let increment = () => setCount(count + 1)
return { count, decrement, increment }
}
let Counter = createContainer(useCounter)
function CounterDisplay() {
let counter = Counter.useContainer()
return (
<div>
<button onClick={counter.decrement}>-</button>
<span>{counter.count}</span>
<button onClick={counter.increment}>+</button>
</div>
)
}
function App() {
return (
<Counter.Provider>
<CounterDisplay />
<CounterDisplay />
</Counter.Provider>
)
}
render(<App />, document.getElementById("root"))
Отношение к Unstated
Я (Jamie Kyle — прим. пер.) рассматриваю данную библиотеку как преемника Unstated. Я сделал Unstated, поскольку был убежден, что React и сам отлично справлялся с управлением состоянием, и ему не хватало только простого механизма для разделения общего состояния и логики. Поэтому я создал Unstated как "минимальное" решение для данной проблемы.
С появлением хуков React стал гораздо лучше в плане выделения общего состояния и логики. Настолько лучше, что, с моей точки зрения, Unstated стал излишней абстракцией.
ТЕМ НЕ МЕНЕЕ, я считаю, что многие разработчики слабо представляют, как разделять логику и общее состояние приложения с помощью React-хуков. Это может быть связано просто с недостаточным качеством документации и инерцией сообщества, но я полагаю, что четкий API как раз способен исправить этот недостаток.
Unstated Next и есть этот самый API. Вместо того, чтобы быть "Минимальным API для разделения общего состояния и логики в React", теперь он "Минимальный API для понимания, как разделять общее состояние и логику в React".
Мне очень нравится React, я хочу, чтобы React процветал. Я бы предпочел, чтобы сообщество отказалось от использования внешних библиотек для управления состоянием наподобие Redux, и начало наконец в полную силу использовать встроенные в React инструменты.
Если вместо того, чтобы использовать Unstated, вы будете просто использовать React — я буду это только приветствовать. Пишите об этом в своих блогах! Выступайте об этом на конференциях! Делитесь своими знаниями с сообществом.
Руководство по Unstated-next
Если вы пока не знакомы с React-хуками, рекомендую прервать чтение и ознакомиться с
прекрасной документацией на сайте React.
Итак, с помощью хуков вы можете написать что-нибудь вроде такого компонента:
function CounterDisplay() {
let [count, setCount] = useState(0)
let decrement = () => setCount(count - 1)
let increment = () => setCount(count + 1)
return (
<div>
<button onClick={decrement}>-</button>
<p>You clicked {count} times</p>
<button onClick={increment}>+</button>
</div>
)
}
Если логику компонента требуется использовать в нескольких местах, ее можно вынести
в отдельный кастомный хук:
function useCounter() {
let [count, setCount] = useState(0)
let decrement = () => setCount(count - 1)
let increment = () => setCount(count + 1)
return { count, decrement, increment }
}
function CounterDisplay() {
let counter = useCounter()
return (
<div>
<button onClick={counter.decrement}>-</button>
<p>You clicked {counter.count} times</p>
<button onClick={counter.increment}>+</button>
</div>
)
}
Но что делать, когда вам требуется общее состояние, а не только логика?
Здесь пригодится контекст:
function useCounter() {
let [count, setCount] = useState(0)
let decrement = () => setCount(count - 1)
let increment = () => setCount(count + 1)
return { count, decrement, increment }
}
let Counter = createContext(null)
function CounterDisplay() {
let counter = useContext(Counter)
return (
<div>
<button onClick={counter.decrement}>-</button>
<p>You clicked {counter.count} times</p>
<button onClick={counter.increment}>+</button>
</div>
)
}
function App() {
let counter = useCounter()
return (
<Counter.Provider value={counter}>
<CounterDisplay />
<CounterDisplay />
</Counter.Provider>
)
}
Это замечательно и прекрасно; чем больше людей будет писать в таком стиле, тем лучше.
Однако стоит внести еще чуть больше структуры и ясности, чтобы API предельно четко выражал ваши намерения.
Для этого мы добавили функцию createContainer()
, чтобы можно было рассматривать ваши кастомные хуки как "контейнеры", чтобы наш четкий и ясный API просто невозможно было использовать неправильно.
import { createContainer } from "unstated-next"
function useCounter() {
let [count, setCount] = useState(0)
let decrement = () => setCount(count - 1)
let increment = () => setCount(count + 1)
return { count, decrement, increment }
}
let Counter = createContainer(useCounter)
function CounterDisplay() {
let counter = Counter.useContainer()
return (
<div>
<button onClick={counter.decrement}>-</button>
<p>You clicked {counter.count} times</p>
<button onClick={counter.increment}>+</button>
</div>
)
}
function App() {
return (
<Counter.Provider>
<CounterDisplay />
<CounterDisplay />
</Counter.Provider>
)
}
Сравните текст компонента до и после наших изменений:
- import { createContext, useContext } from "react"
+ import { createContainer } from "unstated-next"
function useCounter() {
...
}
- let Counter = createContext(null)
+ let Counter = createContainer(useCounter)
function CounterDisplay() {
- let counter = useContext(Counter)
+ let counter = Counter.useContainer()
return (
<div>
...
</div>
)
}
function App() {
- let counter = useCounter()
return (
- <Counter.Provider value={counter}>
+ <Counter.Provider>
<CounterDisplay />
<CounterDisplay />
</Counter.Provider>
)
}
Если вы пишете на TypeScript (а если нет — настоятельно рекомендую ознакомиться с ним), вы ко всему прочему получаете более качественный вывод типов. Если ваш кастомный хук строго типизирован, вывод всех остальных типов сработает автоматически.
API
createContainer(useHook)
import { createContainer } from "unstated-next"
function useCustomHook() {
let [value, setValue] = useState()
let onChange = e => setValue(e.currentTarget.value)
return { value, onChange }
}
let Container = createContainer(useCustomHook)
// Container === { Provider, useContainer }
<Container.Provider>
function ParentComponent() {
return (
<Container.Provider>
<ChildComponent />
</Container.Provider>
)
}
Container.useContainer()
function ChildComponent() {
let input = Container.useContainer()
return <input value={input.value} onChange={input.onChange} />
}
useContainer(Container)
import { useContainer } from "unstated-next"
function ChildComponent() {
let input = useContainer(Container)
return <input value={input.value} onChange={input.onChange} />
}
Советы
Совет #1: Объединение контейнеров
Поскольку мы имеем дело с кастомными хуками, мы можем объединять контейнеры внутри других хуков.
function useCounter() {
let [count, setCount] = useState(0)
let decrement = () => setCount(count - 1)
let increment = () => setCount(count + 1)
return { count, decrement, increment, setCount }
}
let Counter = createContainer(useCounter)
function useResettableCounter() {
let counter = Counter.useContainer()
let reset = () => counter.setCount(0)
return { ...counter, reset }
}
Совет #2: Используйте маленькие контейнеры
Контейнеры лучше всего делать маленькими и четко сфокусированными на конкретной задаче. Если вам нужна дополнительная бизнес-логика в контейнерах — выносите новые операции в отдельные хуки, а состояние пусть хранится в контейнерах.
function useCount() {
return useState(0)
}
let Count = createContainer(useCount)
function useCounter() {
let [count, setCount] = Count.useContainer()
let decrement = () => setCount(count - 1)
let increment = () => setCount(count + 1)
let reset = () => setCount(0)
return { count, decrement, increment, reset }
}
Совет #3: Оптимизация компонентов
Не существует никакой отдельной "оптимизации" для unstated-next
, достаточно обычных приемов оптимизации React-компонентов.
1) Оптимизация тяжелых поддеревьев с помощью разбиения компонентов на части.
До:
function CounterDisplay() {
let counter = Counter.useContainer()
return (
<div>
<button onClick={counter.decrement}>-</button>
<p>You clicked {counter.count} times</p>
<button onClick={counter.increment}>+</button>
<div>
<div>
<div>
<div>СУПЕР НАВОРОЧЕННОЕ ПОДДЕРЕВО КОМПОНЕНТОВ</div>
</div>
</div>
</div>
</div>
)
}
После:
function ExpensiveComponent() {
return (
<div>
<div>
<div>
<div>СУПЕР НАВОРОЧЕННОЕ ПОДДЕРЕВО КОМПОНЕНТОВ</div>
</div>
</div>
</div>
)
}
function CounterDisplay() {
let counter = Counter.useContainer()
return (
<div>
<button onClick={counter.decrement}>-</button>
<p>You clicked {counter.count} times</p>
<button onClick={counter.increment}>+</button>
<ExpensiveComponent />
</div>
)
}
2) Оптимизация тяжелых операций с помощью хука useMemo()
До:
function CounterDisplay(props) {
let counter = Counter.useContainer()
// Вычислять выражение каждый раз, когда обновляется `counter` — слишком медленно
let expensiveValue = expensiveComputation(props.input)
return (
<div>
<button onClick={counter.decrement}>-</button>
<p>You clicked {counter.count} times</p>
<button onClick={counter.increment}>+</button>
</div>
)
}
После:
function CounterDisplay(props) {
let counter = Counter.useContainer()
// Пересчитываем значение только тогда, когда входные данные изменились
let expensiveValue = useMemo(() => {
return expensiveComputation(props.input)
}, [props.input])
return (
<div>
<button onClick={counter.decrement}>-</button>
<p>You clicked {counter.count} times</p>
<button onClick={counter.increment}>+</button>
</div>
)
}
3) Снижаем количество повторных рендеров с помощью React.memo() and useCallback()
До:
function useCounter() {
let [count, setCount] = useState(0)
let decrement = () => setCount(count - 1)
let increment = () => setCount(count + 1)
return { count, decrement, increment }
}
let Counter = createContainer(useCounter)
function CounterDisplay(props) {
let counter = Counter.useContainer()
return (
<div>
<button onClick={counter.decrement}>-</button>
<p>You clicked {counter.count} times</p>
<button onClick={counter.increment}>+</button>
</div>
)
}
После:
function useCounter() {
let [count, setCount] = useState(0)
let decrement = useCallback(() => setCount(count - 1), [count])
let increment = useCallback(() => setCount(count + 1), [count])
return { count, decrement, increment }
}
let Counter = createContainer(useCounter)
let CounterDisplayInner = React.memo(props => {
return (
<div>
<button onClick={props.decrement}>-</button>
<p>You clicked {props.count} times</p>
<button onClick={props.increment}>+</button>
</div>
)
})
function CounterDisplay(props) {
let counter = Counter.useContainer()
return <CounterDisplayInner {...counter} />
}
Миграция с unstated
Я нарочно публикую эту библиотеку как отдельный пакет, потому что весь API полностью новый. Поэтому вы можете параллельно установить оба пакета и мигрировать постепенно.
Поделитесь своими впечатлениями о переходе на unstated-next
, потому что в течение нескольких следующих месяцев я планирую на базе этой информации сделать две вещи:
- Убедиться, что
unstated-next
удовлетворяет все нужды пользователейunstated
. - Удостовериться, что для
unstated
есть четкий и ясный процесс миграции наunstated-next
.
Возможно, я добавлю какие-то API в старую или новую библиотеку, чтобы упростить жизнь разработчикам. Что касается unstated-next
, я обещаю, что добавленные API будут минимальными, насколько это возможно, и я приложу все усилия, чтобы библиотека осталась маленькой.
В будущем, я, вероятно, перенесу код unstated-next
обратно в unstated
в качестве новой мажорной версии. unstated-next
будет по-прежнему доступен, чтобы можно было параллельно пользоваться unstated@2
и unstated-next
в одном проекте. Затем, когда вы закончите миграцию, вы сможете обновиться до версии unstated@3
и удалить unstated-next
(разумеется, обновив все импорты… поиска и замены должно хватить).
Несмотря на кардинальную смену API, я надеюсь, что смогу обеспечить вам максимально простую миграцию, насколько это вообще возможно. Буду рад любым замечаниям о том, что можно было бы сделать лучше.
Ссылки
- npm: unstated-next
- github: unstated-next
Комментарии (8)
artalar
13.05.2019 03:23Подобное решение — абсолютный вендор-лок реакта для приложения. Любой стейт менеджер — это, в первую очередь, способ сделать модель данных и работу с ней обособленно от view. Хотя кто сейчас умет это готовить…
knotri
13.05.2019 11:13А вы просто перестаньте считать реакт View, и отнеситесь к нему как к фреймворку.
UPD: Решил пояснить свою мысль более подробно. Я не говорю что реакт это библиотека (хотя так считаю создатели самого реакта) как не говорю что он фреймворк.
Я скорее о том что его МОЖНО использовать и как то, и как это. На моей практике реакт использовался ближе к понимаю "фреймворк", и это прекрасно работало. Реакт это же не только (props, state) => UI.
Есть контекст, есть возможность использовать HOC для более сложной, чем UI логики. Есть кучу пакетов которые добавляют "фреймворкности" и которые неразрывно связаны с реактом, react-router например. Вы сами загоняете себя в рамки если всегда думаете в стиле "использую реакт только как View часть, чтоб можно было перейти". Вот в данной статье тоже самое, вам показали другой (и возможно более удобный для вашего проекта) стейт менеджер, а вы отказываетесь от него только по причине выше названой
JustDont
13.05.2019 11:35+1Но зачем, если там помимо View ничего интересного нет? Из-за useState считать реакт чем-то шире чистой отображаловки? Ха. Ха.
Более того, реакт в качестве чистой отображаловки куда интереснее реакта для всего. Отображение — это очень сильная сторона в реакте, а всё остальное — на редкость неинтересное.
JustDont
Назначить в противники вещь с известным заметным шлейфом проблем и победоносно закатать её в асфальт — отличный риторический приём. Всегда работает. Даже на технарей.
Главный вопрос: а зачем этот пакет сравнивать именно с Redux? Это выглядит сравнением настоящего слона и фарфоровой статуэтки слона. И то и другое можно назвать «слон», но на этом сходства заканчиваются.
Каждый раз когда я читаю что-нибудь про state management и дохожу вот до таких вот примерчиков — меня начинает преследовать ощущение, что, простите меня за мой французский, меня где-то нае****.
Потому что когда мне нужен state management — он мне нужен вот как раз для этих вещей — убрать страдания над повторными рендерами и прочим накатыванием стейта подальше из значимого кода. Желательно в библиотеку, этому как раз там очень хорошее место. Нафига мне «стейт менеджмент», если мне после этого самого менеджмента всё равно надо закатывать солнце вручную, если я хочу эффективного рендера? Ах, в 200 пожатых байт ничего такого не впихнуть? Ну а зачем мне тогда 200 пожатых байт, если всё что они дают — это тоненький слой сахара?
yallie Автор
Имхо, автор ставит целью показать, что слон во многих проектах вообще не нужен.
Не могу не согласиться. Впрочем, автору плюс, что он этот примерчик сразу честно приводит.