Teaful is a tiny, simple and powerful React state management library
There are many ways to manage state between components in React. For simplicity, the author settled on React Context, but there is a problem. If you change the value of one field, all components that work with state fields are re-rendered.
The Teaful library, called the Fragmented store at the beginning of development, solves this problem. You can see the result on the KDPV. Talking about Teaful while our course on Fullstack Python Development…
What is fragmented-store
Fragmented-store allows you to use each store field separately. Since most components use multiple store fields, it is not interesting to see them re-rendered when other fields are updated.
To solve the problem with React Context, you need to create a context for each field in the store, which is tricky.
// ❌ Not recommended
<UsernameProvider>
<AgeProvider>
{children}
</AgeProvider>
</UsernameProvider>
When there are many fields, to avoid re-rendering, each property needs its own context, which means that you need to write too much logic. However, the context can now be created automatically. A simple and convenient 500 byte library – fragmented-store will help with this.
Create a context and add a Provider
We initialize the store with the data that will be needed at the beginning. Just like with the 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>
);
}
Using one field
Let’s write 2 components working with the store field. It looks like useState
in each component with the desired property. But here they use the same property together with the same value:
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>
);
}
When AgeComponent
updates the field age
, only re-displayed AgeComponent
, a UsernameComponent
does not use the same fragmented part of the store and is not rendered.
Using the entire store
What if you need to update multiple fields? In this case, you need a component that uses the entire store at once. It will be re-rendered for any updated field:
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>
);
}
If you update only some of the fields, then the components working with them will be rendered, but those working with other fields will not:
// 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" })
You don’t need to do this, even if there is a possibility:
update(s => ({ ...s, username: "Aral" }))
So only the components that are for working with the field will be re-rendered. username
use hook useUsername
…
Internal implementation
Library fragmented-store – It is one very short file. You don’t manually write React Context for each property. The library automatically creates everything that is needed to update hooks and other work with them:
Example
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
}
Demo
To give you an idea of how it works, I created a sandbox. I have added a conosle.log to each component so you can see when it redraws. The example is very simple, but you can create your own components and state.
Conclusion
The advantage of fragmented-store is that it works with React Context and there is no need to create many contexts manually.
In the example and in the fragmented-store library, only the first level of fragmentation is still possible. Any improvements on GitHub are greatly appreciated.
Teaful: tiny, lightweight and powerful state management tool in React
Original second part.
We recently rewrote fragmented-store – now it is smaller, simpler, more powerful and is now called Teaful… Since its inception, the library has been called like this:
This is the final title. I’ll tell you about all the listed benefits.
What does less mean?
Teaful less than 1 KBso you don’t have to write a lot of code. This makes the project much easier:
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
What does easier mean?
Working with store properties sometimes requires a lot of boilerplate code: actions, reducers, selectors, connect, etc. Teaful’s goal is to be very easy to use, work with a property and overwrite it without templates. And here’s the result:
What does more powerful mean?
Teaful’s code is easy to maintain, this library eliminates unnecessary rendering and improves site performance. When a single property is updated in the store, only the component that works with that updated property is notified:
Other benefits
On smaller projects, Teaful replaces Redux or Mobx and brings speed. Large projects with it are easier to maintain, and their code does not bloat.
Create store property on the fly
This is how you can use, update, and define new store properties on the fly:
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>
)
}
Working with multiple levels of property nesting
You can work with any property anywhere in the store like this:
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>
)
}
Resetting the store property to its original value
Unlike React hooks like useState
, Teaful has a third element to reset the property to its original value:
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>
)
}
This applies to all levels. This is how you can reset the entire store to its original value:
const [store, setStore, resetStore] = useStore();
// ...
resetStore()
Using multiple stores
Let’s create a few stores and rename the hooks:
import createStore from "teaful";
export const { useStore: useCart } = createStore({ price: 0, items: [] });
export const { useStore: useCounter } = createStore({ count: 0 });
And we use them in the components:
import { useCounter, useCart } from "./store";
function Cart() {
const [price, setPrice] = useCart.price();
// ... rest
}
function Counter() {
const [count, setCount] = useCounter.count();
// ... rest
}
Custom objects for updating the DOM
In order for multiple components to use the same objects with DOM update methods, let’s define them in advance using a helper method 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)
And we use in components:
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>
)
}
Optimistic updates
Thanks to the function onAfterUpdate
it is possible to perform an optimistic update. In other words, you can update the store and store the value by calling the API, and if the call fails, revert to the previous value:
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))
}
}
In this case, you do not need to change the components:
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>
)
}
To make the optimistic update concern only one component, we register it like this:
const [count, setCount] = useStore.count(0, onAfterUpdate);
Store computed properties
To cart.price
was always the computed value of another property, for example from cart.items
, we use the function 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);
}
Again, you don’t need to modify the components:
import { useStore } from "./store";
export function Counter() {
const [price] = useStore.cart.price();
// 6€
return <div>{price}€</div>
}
Find out more about Teaful
I recommend looking into README and read the documentation, see all the options, and find out where to start. There is a section with examples in the documentation, which will be updated.
Teaful is in early stages of development. By version 1.0, the library should be even smaller, lighter and more powerful. The library community is growing rapidly, we welcome any suggestions. I thank everyone who contributed to the Teaful code.
You can continue learning React in our courses:
Find out the details stock.
Other professions and courses
Data Science and Machine Learning
Python, web development
Mobile development
Java and C #
From the basics to the depth
And