Вы находитесь на странице: 1из 9

294

Моя лента Все потоки Разработка Администрирование Дизайн Менеджмент Маркетинг Научпоп

yallie 12 мая 2019 в 14:23

Unstated-next: минималистичное управление состоянием


React-приложения
Автор оригинала: Jamie Kyle

ReactJS

Перевод Tutorial

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

Теги: react, state, state management

Хабы: ReactJS

Вам также может понравиться