Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/teafuljs/teaful

đŸĩ Tiny, easy and powerful React state management
https://github.com/teafuljs/teaful

easy fragmented javascript management performance preact react state state-management store teaful tiny

Last synced: 5 days ago
JSON representation

đŸĩ Tiny, easy and powerful React state management

Awesome Lists containing this project

README

        

Teaful


Teaful

_Tiny, easy and powerful **React state management** library_

[![npm version](https://badge.fury.io/js/teaful.svg)](https://badge.fury.io/js/teaful)
[![gzip size](https://img.badgesize.io/https://unpkg.com/teaful?compression=gzip&label=gzip)](https://unpkg.com/teaful)
[![CI Status](https://github.com/teafuljs/teaful/actions/workflows/test.yml/badge.svg)](https://github.com/teafuljs/teaful/actions/workflows/test.yml)
[![Maintenance Status](https://badgen.net/badge/maintenance/active/green)](https://github.com/teafuljs/teaful#maintenance-status)
[![Weekly downloads](https://badgen.net/npm/dw/teaful?color=blue)](https://www.npmjs.com/package/teaful)
[![GitHub Discussions: Chat With Us](https://badgen.net/badge/discussions/chat%20with%20us/purple)](https://github.com/teafuljs/teaful/discussions)
[![PRs Welcome][badge-prwelcome]][prwelcome]
[![All Contributors](https://img.shields.io/badge/all_contributors-11-orange.svg?style=flat-square)](#contributors-)

[badge-prwelcome]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square
[prwelcome]: http://makeapullrequest.com

## What advantages does it have? ✨


  • đŸ“Ļ ãƒģTiny: Less than 1kb package to manage your state in React and Preact.

  • 🌱 ãƒģEasy: You don't need actions, reducers, selectors, connect, providers, etc. Everything can be done in the simplest and most comfortable way.

  • 🚀 ãƒģPowerful: When a store property is updated, only its components are re-rendered. It's not re-rendering components that use other store properties.



## Guide đŸ—ē

- [1. Installation 🧑đŸģ‍đŸ’ģ](#installation-)
- [2. Init your store 👩đŸŊ‍🎨](#init-your-store-)
- [createStore](#createstore)
- [How to export](#how-to-export)
- [3. Manage the store 🕹](#manage-the-store-)
- [useStore hook](#usestore-hook)
- [setStore helper](#setstore-helper)
- [getStore helper](#getstore-helper)
- [withStore HoC](#withstore-hoc)
- [4. Register events after an update đŸšĻ](#register-events-after-an-update-)
- [5. How to... 🧑‍🎓](#how-to-)
- [Add a new store property](#add-a-new-store-property)
- [Use more than one store](#use-more-than-one-store)
- [Update several portions avoiding rerenders in the rest](#update-several-portions-avoiding-rerenders-in-the-rest)
- [Define calculated properties](#define-calculated-properties)
- [6. Teaful Devtools 🛠](#teaful-devtools-)
- [7. Addons and extras 🌀](#addons-and-extras-)
- [8. Examples đŸ–Ĩ](#examples-)
- [9. Roadmap đŸ›Ŗ](#roadmap-)
- [10. Contributors ✨](#contributors-)

## Installation 🧑đŸģ‍đŸ’ģ

```sh
yarn add teaful
# or
npm install teaful --save
```

## Init your store 👩đŸŊ‍🎨

Each store has to be created with the `createStore` function. This function returns all the methods that you can use to consume and update the store properties.

### createStore

```js
import createStore from "teaful";

const { useStore } = createStore();
```

Or also with an initial store:

```js
const initialStore = {
cart: { price: 0, items: [] },
};
const { useStore, getStore } = createStore(initialStore);
```

Or also with an event that is executed after every update:

```js
const initialStore = {
cart: { price: 0, items: [] },
};

function onAfterUpdate({ store, prevStore }) {
console.log("This callback is executed after an update");
}

const { useStore } = createStore(initialStore, onAfterUpdate);
```

_Input:_

| name | type | required | description |
| --------------- | ------------- | -------- | -------------------------------------------------------------------------------------------------------- |
| `initialStore` | `object` | `false` | Object with your initial store. |
| `onAfterUpdate` | `function` | `false` | Function that is executed after each property change. More [details](#register-events-after-an-update-). |

_Output:_

| name | type | description | example |
| ----------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
| `useStore` | `Proxy` | Proxy hook to consume and update store properties inside your components. Each time the value changes, the component is rendered again with the new value. More [info](#usestore-hook). | `const [price, setPrice] = useStore.cart.price()` |
| `getStore` | `Proxy` | Similar to `useStore` but without subscription. You can use it as a helper outside (or inside) components. Note that if the value changes, it does not cause a rerender. More [info](#getstore-helper). | `const [price, setPrice] = getStore.cart.price()` |
| `setStore` | `Proxy` | It's a proxy helper to modify a store property outside (or inside) components. More [info](#setstore-helper). | `setStore.user.name('Aral')` or `setStore.cart.price(price => price + 10)` |
| `withStore` | `Proxy` | HoC with `useStore` inside. Useful for components that are not functional. More [info](#withstore-hoc). | `withStore.cart.price(MyComponent)` |

### How to export

We recommend using this type of export:

```js
// ✅
export const { useStore, getStore, withStore } = createStore({
cart: { price: 0, items: [] },
});
```

This way you can import it with:

```js
// ✅
import { useStore } from '../store'
```

Avoid using a default export with all:

```js
// ❌
export default createStore({ cart: { price: 0, items: [] } });
```

Because then you won't be able to do this:

```js
// ❌ It's not working well with proxies
import { useStore } from '../store'
```

## Manage the store 🕹

### useStore hook

It's recommended to use the `useStore` hook as a proxy to indicate exactly what **portion of the store** you want. This way you only subscribe to this part of the store avoiding unnecessary re-renders.

```js
import createStore from "teaful";

const { useStore } = createStore({
username: "Aral",
count: 0,
age: 31,
cart: {
price: 0,
items: [],
},
});

function Example() {
const [username, setUsername] = useStore.username();
const [cartPrice, setCartPrice] = useStore.cart.price();

return (
<>
setUsername("AnotherUserName")}>
Update {username}

setCartPrice((v) => v + 1)}>
Increment price: {cartPrice}â‚Ŧ

>
);
}
```

However, it's also possible to use the `useStore` hook to use **all the store**.

```js
function Example() {
const [store, setStore] = useStore();

return (
<>

setStore((s) => ({
...s,
username: "AnotherUserName",
}))
}
>
Update {store.username}


setStore((s) => ({
...s,
cart: { ...s.cart, price: s.cart.price + 1 },
}))
}
>
Increment price: {store.cart.price}â‚Ŧ

>
);
}
```

_Input:_

| name | type | description | example |
| --------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Initial value | `any` | This parameter is **not mandatory**. It only makes sense for new store properties that have not been defined before within the `createStore`. If the value has already been initialized inside the `createStore` this parameter has no effect. | `const [price, setPrice] = useStore.cart.price(0)` |
| event after an update | `function` | This parameter is **not mandatory**. Adds an event that is executed every time there is a change inside the indicated store portion. | `const [price, setPrice] = useStore.cart.price(0, onAfterUpdate)`

And the function:
`function onAfterUpdate({ store, prevStore }){ console.log({ store, prevStore }) }`
|

_Output:_

Is an `Array` with **2** items:

| name | type | description | example |
| ------------ | ---------- | --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| value | `any` | The value of the store portion indicated with the proxy. | A store portion

`const [price] = useStore.cart.price()`
All store:
`const [store] = useStore()`
|
| update value | `function` | Function to update the store property indicated with the proxy. | Updating a store portion:
`const [count, setCount] = useStore.count(0)`
Way 1:
`setCount(count + 1)`
Way 1:
`setCount(c => c + 1)`
-------
Updating all store:
`const [store, updateStore] = useStore()`
Way 1:
`updateStore({ ...store, count: 2 }))`
Way 1:
`updateStore(s => ({ ...s, count: 2 }))`
|

### setStore helper

Useful helper to modify the store from anywhere (outside/inside components).

Example:

```js
const initialStore = { count: 0, name: 'Aral' }
const { setStore } = createStore(initialStore);

const resetStore = () => setStore(initialStore);
const resetCount = () => setStore.count(initialStore.count);
const resetName = () => setStore.name(initialStore.name);

// Component without any re-render (without useStore hook)
function Resets() {
return (
<>

Reset store


Reset count


Reset name

>
);
}
```

Another example:

```js
const { useStore, setStore } = createStore({
firstName: '',
lastName: ''
});

function ExampleOfForm() {
const [formFields] = useStore()

return Object.entries(formFields).map(([key, value]) => (
{
// Update depending the key attribute
setStore[key](e.target.value)
}}
/>
))
}
```

This second example only causes re-renders in the components that consume the property that has been modified.

In this way:

```js
const [formFields, setFormFields] = useStore()
// ...
setFormFields(s => ({ ...s, [key]: e.target.value })) // ❌
```

This causes a re-render on all components that are consuming any of the form properties, instead of just the one that has been updated. So using the `setStore` proxy helper is more recommended.

### getStore helper

It works exactly like `useStore` but with **some differences**:

- It **does not make a subscription**. So it is no longer a hook and you can use it as a helper wherever you want.
- It's **not possible to register events** that are executed after a change.

```js
getStore.cart.price(0, onAfterPriceChange); // ❌

function onAfterPriceChange({ store, prevStore }) {
// ...
}
```

- If the intention is to register events that last forever, it has to be done within the `createStore`:

```js
const { getStore } = createStore(initialStore, onAfterUpdate); // ✅

function onAfterUpdate({ store, prevStore }) {
// ..
}
```

Very useful to use it:

- **Outside components**: helpers, services, etc.
- **Inside components**: Avoiding rerenders if you want to consume it inside events, when you only use the updater `const [, setCount] = getStore.count()`, etc.

Example:

```js
import { useState } from "react";

const initialStore = { count: 0 }
const { getStore } = createStore(initialStore);

function Example1() {
return (
{
const [, setStore] = getStore();
setStore(initialStore)
}}>
Reset store

);
}

function Example2() {
const [newCount, setNewCount] = useState();

function saveIncreasedCount(e) {
e.preventDefault();
const [count, setCount] = getStore.count();
if (newCount > count) setCount(newCount);
else alert("You should increase the value");
}

return (

setNewCount(e.target.valueAsNumber)}
type="number"
/>
Save the increased count value

);
}
```

### withStore HoC

It's a wrapper of the `useStore` for non-functional components. Where you receive the same thing that the `useStore` hook returns inside `this.props.store`.

Example with a store portion:

```js
const { withStore } = createStore();

class Counter extends Component {
render() {
const [count, setCount] = this.props.store;
return (


{count}


setCount((v) => v + 1)}>+
setCount((v) => v - 1)}>-
setCount(0)}>reset

);
}
}

// Similar to useStore.counter.count(0)
const CounterWithStore = withStore.counter.count(Counter, 0);
```

Example with all store:

```js
const { withStore } = createStore({ count: 0 });

class Counter extends Component {
render() {
const [store, setStore] = this.props.store;
return (


{store.count}


setStore({ count: store.count + 1 })}>+
setStore({ count: store.count - 1 })}>-
setStore({ count: 0 })}>reset

);
}
}

// Similar to useStore()
const CounterWithStore = withStore(Counter);
```

The **only difference** with the `useStore` is that instead of having 2 parameters (initialValue, onAfterUpdate), it has 3 where the **first one is mandatory** and the other 2 are not (**Component**, **initialValue**, **onAfterUpdate**).

## Register events after an update đŸšĻ

It is possible to register an event after each update. This can be useful for validating properties, storing error messages, optimistic updates...

There are 2 ways to register:

- **Permanent** events: Inside `createStore`. This event will always be executed for each change made within the store.

```js
export const { useStore, getStore } = createStore(
initialStore,
onAfterUpdate
);

function onAfterUpdate({ store, prevStore }) {
// Add an error msg
if (store.count > 99 && !store.errorMsg) {
const [, setErrorMsg] = getStore.errorMsg();
setErrorMsg("The count value should be lower than 100");
return;
}
// Remove error msg
if (store.count <= 99 && store.errorMsg) {
const [, setErrorMsg] = getStore.errorMsg();
setErrorMsg();
}
}
```

- **Temporal** events: Inside `useStore` / `withStore`. These events will be executed for each change in the store (or indicated portion) **only during the life of the component**, when the component is unmounted the event is removed.

```js
function Count() {
const [count, setCount] = useStore.count(0, onAfterUpdate);
const [errorMsg, setErrorMsg] = useStore.errorMsg();

// The event lasts as long as this component lives
function onAfterUpdate({ store, prevStore }) {
// Add an error msg
if (store.count > 99 && !store.errorMsg) {
setErrorMsg("The count value should be lower than 100");
return;
}
// Remove error msg
if (store.count >= 99 && store.errorMsg) {
setErrorMsg();
}
}

return (
<>
{errorMsg &&

{errorMsg}
}
{count}

setCount((v) => v + 1)}>Increment
>
);
}
```

## How to... 🧑‍🎓

### Add a new store property

You can use `useStore` / `getStore` / `withStore` even if the property does not exist inside the store, and create it on the fly.

```js
const { useStore } = createStore({ username: "Aral" });

function CreateProperty() {
const [price, setPrice] = useStore.cart.price(0); // 0 as initial value

return

Price: {price}
;
}

function OtherComponent() {
// store now is { username: 'Aral', cart: { price: 0 } }
const [store] = useStore();
console.log(store.cart.price); // 0
// ...
}
```

It's **not mandatory to indicate the initial value**, you can create the property in a following step with the updater.

```js
const { useStore } = createStore({ username: "Aral" });

function CreateProperty() {
const [cart, setCart] = useStore.cart();

useEffect(() => {
initCart();
}, []);
async function initCart() {
const newCart = await fetch("/api/cart");
setCart(newCart);
}

if (!cart) return null;

return

Price: {cart.price}
;
}
```

### Use more than one store

You can have as many stores as you want. The only thing you have to do is to use as many `createStore` as stores you want.

store.js

```js
import createStore from "teaful";

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

Cart.js

```js
import { useCart } from "./store";

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

Counter.js

```js
import { useCounter } from "./store";

export default function Counter() {
const [count, setCount] = useCounter.count();
// ... rest
}
```

### Update several portions avoiding rerenders in the rest

If you do this it causes a rerender to all the properties of the store:

```js
// 😡
const [store, setStore] = useStore();
setStore({ ...store, count: 10, username: "" });
```

And if you do the next, you convert the whole store into only 2 properties (`{ count: 10, username: '' }`), and you will remove the rest:

```js
// đŸĨĩ
const [store, setStore] = useStore();
setStore({ count: 10, username: "" });
```

If you have to update several properties and you don't want to disturb the rest of the components that are using other store properties you can create a helper with `getStore`.

```js
export const { useStore, setStore } = createStore(initialStore);

export function setFragmentedStore(fields) {
Object.entries(fields).forEach(([key, value]) => {
setStore[key](value);
});
}
```

And use it wherever you want:

```js
// 🤩
import { setFragmentedStore } from "./store";

// ...
setFragmentedStore({ count: 10, username: "" });
```

### Define calculated properties

It's possible to use the `setStore` together with the function that is executed after each update to have store properties calculated from others.

In this example the cart price value will always be a value calculated according to the array of items:

```js
export const { useStore, setStore } = createStore(
{
cart: {
price: 0,
items: [],
},
},
onAfterUpdate
);

function onAfterUpdate({ store }) {
const { items, price } = store.cart;
const calculatedPrice = items.length * 3;

// Price always will be items.length * 3
if (price !== calculatedPrice) {
setStore.cart.price(calculatedPrice);
}
}
```

It's an anti-pattern? Not in Teaful 😊. As only the fragments of the store are updated and not the whole store, it is the same as updating both properties (`cart.items` and `cart.price`) instead of just `cart.items`. The anti-pattern comes when it causes unnecessary rerenders, but this is not the case. Only the components that use `cart.items` and `cart.price` are rerendered and not the others.

## Teaful Devtools 🛠

To debug your stores, you can use [Teaful DevTools](https://github.com/teafuljs/teaful-devtools).

Teaful DevTools

## Addons and extras 🌀

To facilitate the creation of libraries that extend Teaful (such as [`teaful-devtools`](https://github.com/teafuljs/teaful-devtools)), we allow the possibility to add an extra that:

- Have access to everything returned by each `createStore` consumed: `getStore`, `useStore`, `withStore`.
- Return an object with new elements to be returned by each `createStore`. It's optional, if nothing is returned it will continue to return the usual. If, for example, you return `{ getCustomThing }` will do an assign with what is currently returned by the `createStore`.
- Ability to **subscribe**, **unsubscribe** and **notify** in each `createStore`.

For that, use the `createStore.ext` function.

teaful-yourlib:

```js
import createStore from 'teaful'

createStore.ext(({ getStore, }, subscription) => {
// s = subscribe (minified by Teaful)
// "." -> all store
// ".cart" -> only inside cart
// ".cart.price" -> only inside cart.price
// n = notify (minified by Teaful)
// u = unsubscribe (minified by Teaful)
subscription.s(".", ({ store, prevStore }) => {
// This will be executed in any store (".") change.
});

// optional
return { getCustomThing: () => console.log('example') }
})
```

Then, your library should be imported at the top:

```js
import 'teaful-yourlib'
import { render } from 'preact';
import App from './components/App';

render(, document.getElementById('root'));
```

## Examples đŸ–Ĩ

We will expand the examples over time. For now you can use this Codesandbox:

- [Example of store - CodeSandbox](https://codesandbox.io/s/teaful-example-4p5dv?file=/src/store.js)
- [Counter with ESM](https://github.com/teafuljs/teaful/tree/master/examples/counter-with-esm)
- [Todo list](https://github.com/teafuljs/teaful/tree/master/examples/todo-list)
- [Example with an API list](https://github.com/teafuljs/teaful/blob/master/examples/api-list/README.md)

## Roadmap đŸ›Ŗ

**For 1.0**:

- [x] React support
- [x] Teaful DevTools
- [x] TypeScript types support
- [x] Migrate full Teaful project to TypeScript
- [ ] React Native support
- [ ] Vanilla JavaScript support
- [ ] Create a documentation website
- [ ] Add more examples: with Next.js, Remix, Preact, React Native...

**Optional for 1.0 (else +1.0):**

- [ ] Svelte support
- [ ] Vue support
- [ ] Solid support

_If you think that there is something that should be preindicated by version 1.0 please report it as an issue or discussion 🙏_

## Contributors ✨

Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):




Aral Roca Gomez

🚧 đŸ’ģ

Danielo Artola

🚇 đŸ’ģ

Yuki Shindo

🚇

YONGJAE LEE(ė´ėšŠėžŦ)

🐛

niexq

📖 🚇

nekonako

📖

Shubham

📖



Siddharth Borderwala

📖 🚇 đŸ’ģ

watcher

đŸ’ģ

Philippe Schommers

đŸ’ģ

ktdd

📖

This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!