
Способов управления состоянием между компонентами в React множество. Из-за простоты автор остановился на React Context, но есть проблема. Если изменить значение одного поля, повторно будут отрисованы все компоненты, работающие с полями состояния.
Библиотека Teaful, которая в начале разработки называлась Fragmented store, решает эту проблему. Результат вы видите на КДПВ. Рассказываем о Teaful, пока начинается наш курс по Fullstack-разработке на Python.

Что такое fragmented-store
Fragmented-store позволяет использовать каждое поле store отдельно. Поскольку большинство компонентов использует несколько полей store, неинтересно, чтобы они заново рендерились при обновлении других полей.

Чтобы решить проблему с помощью React Context, создать контекст нужно для каждого поля store, а это сложно.
// ❌ Not recommended <UsernameProvider> <AgeProvider> {children} </AgeProvider> </UsernameProvider>
Когда полей много, чтобы избежать повторного рендеринга, каждому свойству необходим свой контекст, а значит, нужно написать слишком много логики. Однако теперь контекст можно создать автоматически. Поможет в этом простая и удобная библиотека на 500 байт — fragmented-store.
Создаём контекст и добавляем Provider
Инициализируем store данными, которые понадобятся вначале. Точно так же, как с контекстом React Context:
import createStore from "fragmented-store"; // It is advisable to set all the fields. If you don't know the // initial value you can set it to undefined or null to be able // to consume the values in the same way const { Provider } = createStore({ username: "Aral", age: 31, }); function App() { return ( <Provider> {/* rest */} </Provider> ); }
Используем одно поле
Напишем 2 работающих с полем store компонента. Это похоже на useState в каждом компоненте с нужным свойством. Но здесь они вместе задействуют одно и то же свойство с одинаковым значением:
import createStore from "fragmented-store"; // We can import hooks with the property name in camelCase. // username -> useUsername // age -> useAge const { Provider, useUsername, useAge } = createStore({ username: "Aral", age: 31, }); function App() { return ( <Provider> <UsernameComponent /> <AgeComponent /> </Provider> ); } // Consume the "username" field function UsernameComponent() { const [username, setUsername] = useUsername(); return ( <button onClick={() => setUsername("AnotherUserName")}> Update {username} </button> ); } // Consume the "age" field function AgeComponent() { const [age, setAge] = useAge(); return ( <div> <div>{age}</div> <button onClick={() => setAge((s) => s + 1)}>Inc age</button> </div> ); }
Когда AgeComponent обновляет поле age, повторно отображается только AgeComponent, а UsernameComponent не использует ту же фрагментированную часть store и не отрисовывается.
Используем весь store
Что, если нужно обновить несколько полей? В таком случае нужен компонент, задействующий сразу весь store. Он повторно отрисуется для любого обновлённого поля:
import createStore from "fragmented-store"; // Special hook useStore const { Provider, useStore } = createStore({ username: "Aral", age: 31, }); function App() { return ( <Provider> <AllStoreComponent /> </Provider> ); } // Consume all fields of the store function AllStoreComponent() { const [store, update] = useStore(); console.log({ store }); // all store function onClick() { update({ age: 32, username: "Aral Roca" }) } return ( <button onClick={onClick}>Modify store</button> ); }
Если обновить только некоторые поля, то работающие с ними компоненты будут отрисованы, а работающие с другими полями — нет:
// It only updates the "username" field, other fields won't be updated // The UsernameComponent is going to be re-rendered while AgeComponent won't :) update({ username: "Aral Roca" })
Не нужно делать так, даже если есть возможность:
update(s => ({ ...s, username: "Aral" }))
Так повторно будут отрисованы только компоненты, которые для работы с полем username используют хук useUsername.
Внутренняя реализация
Библиотека fragmented-store — это один очень короткий файл. Вы не пишете контексты React Context для каждого свойства вручную. Библиотека автоматически создаёт всё, что нужно для обновления хуков и другой работы с ними:
Пример
import React, { useState, useContext, createContext } from 'react' export default function createStore(store = {}) { const keys = Object.keys(store) const capitalize = (k) => `${k[0].toUpperCase()}${k.slice(1, k.length)}` // storeUtils is the object we'll return with everything // (Provider, hooks) // // We initialize it by creating a context for each property and // returning a hook to consume the context of each property const storeUtils = keys.reduce((o, key) => { const context = createContext(store[key]) // Property context const keyCapitalized = capitalize(key) if (keyCapitalized === 'Store') { console.error( 'Avoid to use the "store" name at the first level, it\'s reserved for the "useStore" hook.' ) } return { ...o, // All contexts contexts: [...(o.contexts || []), { context, key }], // Hook to consume the property context [`use${keyCapitalized}`]: () => useContext(context), } }, {}) // We create the main provider by wrapping all the providers storeUtils.Provider = ({ children }) => { const Empty = ({ children }) => children const Component = storeUtils.contexts .map(({ context, key }) => ({ children }) => { const ctx = useState(store[key]) return <context.Provider value={ctx}>{children}</context.Provider> }) .reduce( (RestProviders, Provider) => ({ children }) => ( <Provider> <RestProviders>{children}</RestProviders> </Provider> ), Empty ) return <Component>{children}</Component> } // As a bonus, we create the useStore hook to return all the // state. Also to return an updater that uses all the created hooks at // the same time storeUtils.useStore = () => { const state = {} const updates = {} keys.forEach((k) => { const [s, u] = storeUtils[`use${capitalize(k)}`]() state[k] = s updates[k] = u }) function updater(newState) { const s = typeof newState === 'function' ? newState(state) : newState || {} Object.keys(s).forEach((k) => updates[k] && updates[k](s[k])) } return [state, updater] } // Return everything we've generated return storeUtils }
Демо
Чтобы вы поняли, как это работает,я создал песочницу. В каждый компонент я добавил conosle.log, так что вы сможете увидеть, когда он перерисовывается. Пример очень простой, но вы сможете создать свои компоненты и состояние.
Заключение
Преимущество fragmented-store заключается в том, что она работает с React Context и нет необходимости создавать много контекстов вручную.
В примере и в библиотеке fragmented-store пока возможен лишь первый уровень фрагментации. Приветствуются любые улучшения на GitHub.
Teaful: крошечное, лёгкое и мощное средство управления состоянием на React
Оригинал второй части.
Недавно мы переписали fragmented-store — теперь она меньше, проще, мощнее и теперь называется Teaful. С момента создания библиотека называлась так:
Fragmented-store->Fragstore-> Teaful(Teaful GitHub).

Это окончательное название. Расскажу обо всех перечисленных преимуществах.
Что значит меньше?
Teaful меньше 1 Кб, так что много кода писать не нужно. Это делает проект намного легче:
874 B: index.js.gz 791 B: index.js.br 985 B: index.modern.js.gz 888 B: index.modern.js.br 882 B: index.m.js.gz 799 B: index.m.js.br 950 B: index.umd.js.gz 856 B: index.umd.js.br
Что значит проще?
Для работы со свойствами store иногда требуется много шаблонного кода: экшены, редьюсеры, селекторы, connect и т. д. Цель Teaful — быть очень лёгкой в использовании, работать со свойством и перезаписывать его без шаблонов. И вот результат:

Что значит мощнее?
Код на Teaful легко сопровождать, эта библиотека избавляет от лишних отрисовок и улучшает производительность сайта. Когда в store обновляется единственное свойство, уведомляется только компонент, который работает с этим обновлённым свойством:

Другие преимущества
В небольших проектах Teaful заменяет Redux или Mobx и дарит скорость. Большие проекты с ней сопровождать легче, а их код не раздувается.
Создание свойства store на лету
Вот так можно использовать, обновлять и определять новые свойства store на лету:
const { useStore } = createStore() export function Counter() { const [count, setCount] = useStore.count(0); // 0 as initial value return ( <div> <span>{count}</span> <button onClick={() => setCount(c => c + 1)}> Increment counter </button> <button onClick={() => setCount(c => c - 1)}> Decrement counter </button> </div> ) }
Работа с несколькими уровнями вложения свойств
Работать с любым свойством в любом месте store можно так:
const { useStore } = createStore({ username: "Aral", counters: [ { name: "My first counter", counter: { count: 0 } } ] }) export function Counter({ counterIndex = 0 }) { const [count, setCount] = useStore.counters[counterIndex].counter.count(); return ( <div> <span>{count}</span> <button onClick={() => setCount(c => c + 1)}> Increment counter </button> <button onClick={() => setCount(c => c - 1)}> Decrement counter </button> </div> ) }
Сброс свойства store в исходное значение
В отличие от хуков React типа useState, в Teaful для сброса свойства в исходное значение есть третий элемент:
const { useStore } = createStore({ count: 0 }) export function Counter() { const [count, setCount, resetCounter] = useStore.count(); return ( <div> <span>{count}</span> <button onClick={() => setCount(c => c + 1)}> Increment counter </button> <button onClick={() => setCount(c => c - 1)}> Decrement counter </button> <button onClick={resetCounter}> Reset counter </button> </div> ) }
Это касается всех уровней. Вот так можно вернуть в исходное значение весь store:
const [store, setStore, resetStore] = useStore(); // ... resetStore()
Использование нескольких store
Давайте создадим несколько store и переименуем хуки:
import createStore from "teaful"; export const { useStore: useCart } = createStore({ price: 0, items: [] }); export const { useStore: useCounter } = createStore({ count: 0 });
И задействуем их в компонентах:
import { useCounter, useCart } from "./store"; function Cart() { const [price, setPrice] = useCart.price(); // ... rest } function Counter() { const [count, setCount] = useCounter.count(); // ... rest }
Кастомные объекты для обновления DOM
Чтобы несколько компонентов применяли одни и те же объекты с методами обновления DOM, определим их заранее с помощью вспомогательного метода getStore:
import createStore from "teaful"; export const { useStore, getStore } = createStore({ count: 0 }); const [, setCount] = getStore.count() export const incrementCount = () => setCount(c => c + 1) export const decrementCount = () => setCount(c => c - 1)
И используем в компонентах:
import { useStore, incrementCount, decrementCount } from "./store"; export function Counter() { const [count] = useStore.count(); return ( <div> <span>{count}</span> <button onClick={incrementCount}> Increment counter </button> <button onClick={decrementCount}> Decrement counter </button> </div> ) }
Оптимистичные обновления
Благодаря функции onAfterUpdate возможно выполнить оптимистичное обновление. Иными словами, можно обновить store и сохранить значение, вызывая API, а при сбое вызова вернуться к предыдущему значению:
import createStore from "teaful"; export const { useStore, getStore } = createStore({ count: 0 }, onAfterUpdate); function onAfterUpdate({ store, prevStore }) { if(store.count !== prevStore.count) { const [count, setCount, resetCount] = getStore.count() fetch('/api/count', { method: 'PATCH', body: count }) .catch(e => setCount(prevStore.count)) } }
При этом не нужно изменять компоненты:
import { useStore } from "./store"; export function Counter() { const [count, setCount] = useStore.count(); return ( <div> <span>{count}</span> <button onClick={() => setCount(c => c + 1)}> Increment counter </button> <button onClick={() => setCount(c => c - 1)}> Decrement counter </button> </div> ) }
Чтобы оптимистичное обновление касалось только одного компонента, зарегистрируем его таким образом:
const [count, setCount] = useStore.count(0, onAfterUpdate);
Вычисляемые свойства store
Чтобы cart.priceвсегда был вычисленным значением другого свойства, например из cart.items, используем функцию onAfterUpdate:
export const { useStore, getStore } = createStore( { cart: { price: 0, items: ['apple', 'banana'], }, }, onAfterUpdate, ); function onAfterUpdate({ store, prevStore }) { calculatePriceFromItems() // ... } function calculatePriceFromItems() { const [price, setPrice] = getStore.cart.price(); const [items] = getStore.cart.items(); const calculatedPrice = items.length * 3; if (price !== calculatedPrice) setPrice(calculatedPrice); }
И снова не нужно изменять компоненты:
import { useStore } from "./store"; export function Counter() { const [price] = useStore.cart.price(); // 6€ return <div>{price}€</div> }
Узнайте больше о Teaful
Я рекомендую заглянуть в README и ознакомиться с документацией, посмотреть все варианты и узнать, с чего начать. В документации есть раздел с примерами, который будет пополняться.
Teaful находится на ранней стадии разработки. К версии 1.0 библиотека должна стать ещё меньше, легче и мощнее. Сообщество библиотеки быстро растёт, мы приветствуем любые предложения. Я благодарю всех, кто внёс свой вклад в код Teaful.
Продолжить изучение React вы сможете на наших курсах:

Узнайте подробности акции.
Другие профессии и курсы
Data Science и Machine Learning
Python, веб-разработка
Мобильная разработка
Java и C#
От основ — в глубину
А также

