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


Unfragmented state in React Context
Unfragmented state in React Context

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.

Fragmented-store in React Context
Fragmented-store in React Context

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:

New Teaful logo
New Teaful logo

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:

Teaful: easy to use, no boilerplate code
Teaful: easy to use, no boilerplate code

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:

Teaful re-renders
Teaful re-renders

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.pricewas 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

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *