DEV Community

Cover image for Teaful: tiny, easy and powerful React state management
Aral Roca
Aral Roca

Posted on • Originally published at aralroca.com

Teaful: tiny, easy and powerful React state management

Original article: https://aralroca.com/blog/teaful

I've recently talked about Fragmented-store in another article, a library I was developing, explaining future improvements. Well, we have made a reimplementation to make it tinier, easier to use and more powerful, and we have renamed it to Teaful. In this article I will talk about its benefits and how to use it.

This is the final name. Since the library was created, it has been called:

Teaful logo

Teaful new logo

Why tiny?

Teaful is less than 1kb and you won't need to write so much code. In other words, it will make your project much more lightweight.

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
Enter fullscreen mode Exit fullscreen mode

Why easy?

To consume and modify store properties sometimes requires a lot of boilerplate: actions, reducers, selectors, connect, etc. Teaful's goal is to be very easy to use, to consume a property and overwrite it without any boilerplate. "Tiny" and "powerful", because if you have to write a lot to do a simple thing, your project takes more kb and becomes less maintainable.

Teaful easy to use


Teaful: Easy to use without boilerplate

Why powerful?

Besides doing the code more maintainable, you avoid many unnecessary rerenders while the performance of your website gets better. When you only update one property of the store, it's not necessary to notify all the components that use the store. It only requires to notify who's consuming that updated property.

Teaful rerenders


Teaful rerenders

What other benefits does it have?

In this section of the article we'll see a few of the many things that can be done.

If you use Teaful in a small project you'll be able to move fast without tools like Redux or Mobx that can be overkill. Also, if you use it in large projects they will be more maintainable and won't grow in code.

Creating store properties on the fly

You can consume and update nonexistent store properties and define them 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>
  )
}
Enter fullscreen mode Exit fullscreen mode

Works with heavily nested properties

You can consume / manipulate any property wherever it is in the 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>
  )
}
Enter fullscreen mode Exit fullscreen mode

Reseting a store property to the initial value

Unlike hooks like React's useState, in Teaful there is a third element to reset the property to its initial 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>
  )
}
Enter fullscreen mode Exit fullscreen mode

It's in all levels, if you want to reset the whole store to its initial value you can do it with:

const [store, setStore, resetStore] = useStore();
// ...
resetStore()
Enter fullscreen mode Exit fullscreen mode

Using more than one store

Although it is not necessary, you can have several stores and rename the hooks with more personalized names.

import createStore from "teaful";

export const { useStore: useCart } = createStore({ price: 0, items: [] });
export const { useStore: useCounter } = createStore({ count: 0 });
Enter fullscreen mode Exit fullscreen mode

You can also use them in your components:

import { useCounter, useCart } from "./store";

function Cart() {
  const [price, setPrice] = useCart.price();
  // ... rest
}

function Counter() {
  const [count, setCount] = useCounter.count();
  // ... rest
}
Enter fullscreen mode Exit fullscreen mode

Β Using customized updaters

If you want several components to use the same updaters without reimplementing them, you can predefine them thanks to the getStore helper.

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)
Enter fullscreen mode Exit fullscreen mode

And use them in your 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>
  )
}
Enter fullscreen mode Exit fullscreen mode

Optimistic updates

If you want to make an optimistic update (when you update the store and save the value by calling the api, if the api request fails revert to the previous value). You can do it thanks to the onAfterUpdate function.

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))
  }
}
Enter fullscreen mode Exit fullscreen mode

Your components won't need any changes:

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

If you want the optimistic update to be only for one component and not for all, you can register it with:

const [count, setCount] = useStore.count(0, onAfterUpdate);
Enter fullscreen mode Exit fullscreen mode

Calculated store properties

If we want the cart.price to always be a precomputed value of another property, for example from cart.items, we can do it in the onAfterUpdate function.

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);
}
Enter fullscreen mode Exit fullscreen mode

Your components won't need any changes:

import { useStore } from "./store";

export function Counter() {
  const [price] = useStore.cart.price();

  // 6€
  return <div>{price}€</div>
}
Enter fullscreen mode Exit fullscreen mode

Learn more about Teaful

If you want to try it out, I encourage you to go to the README to read the Teaful documentation, see all the options and learn how to get started. There is also an example section where you can try it. We will upload more examples over time.

Conclusions

Teaful is still at an early stage (version 0.x), so there may still be several improvements in the library to make version 1.0 even more tiny, easy and powerful. Any contribution to the library or suggestions will be very welcome.

For the short life of the library, the community is growing fast and I thank all those who have contributed.

@danielart, @niexq, @shinshin86, @dididy. πŸ‘ 😊

Top comments (3)

Collapse
 
eatsjobs profile image
Pasquale Mangialavori • Edited

Hi Aral. Nice work. I followed your previous implementation and I saw is very different from this one: you're using proxy now for example. Could you tell me why? Have you found the previous implementation limited somehow?

Collapse
 
aralroca profile image
Aral Roca

Thanks! Previously, hooks were generated according to store properties (useCart, useUsername...). It was useful only at the first level, but there could be many conflicts in nested properties.

The implementation has also changed so that instead of creating a Provider for each property (many contexts) we make our subscription system, being more scalable for large stores.

Changing it to a Proxy adds the possibility of using hooks for each store property even if it is very nested. Making the component that has useStore.cart.price() only rerender when the cart price is updated and not when other properties of the store are updated.

It even allows you to access an item in an array that you have in the store and the component will only rerender if that item changes (useStore.items[index]()).

Here there is a tweet I explain a little bit the evolution of this lib twitter.com/aralroca/status/145697...

Collapse
 
eatsjobs profile image
Pasquale Mangialavori

I was asking cause I found it useful and I wrote and implementation very similar using useReducer instead of useState and splitted the dispatch in a different context. I will share it with you soon