All Projects โ†’ teafuljs โ†’ teaful

teafuljs / teaful

Licence: MIT license
๐Ÿต Tiny, easy and powerful React state management

Programming Languages

javascript
184084 projects - #8 most used programming language

Projects that are alternatives of or similar to teaful

Store
A beautifully-simple framework-agnostic modern state management library.
Stars: โœญ 204 (-68.03%)
Mutual labels:  management, state, store
okito
Your best flutter coding friend. All in one; state management, navigation management(with dynamic routing), local storage, localization, dependency injection, cool extensions with best usages and with the support of best utilities!
Stars: โœญ 37 (-94.2%)
Mutual labels:  state-management, state, store
Statty
A tiny and unobtrusive state management library for React and Preact apps
Stars: โœญ 516 (-19.12%)
Mutual labels:  preact, management, state
vue-reactive-store
A VueX alternative : declarative + reactive + centralized way to structure your data store. Inspired by VueX and Vue.js . Compatible with vue-devtools.
Stars: โœญ 27 (-95.77%)
Mutual labels:  state-management, state, store
Reatom
State manager with a focus of all needs
Stars: โœญ 567 (-11.13%)
Mutual labels:  state-management, state, store
RxReduxK
Micro-framework for Redux implemented in Kotlin
Stars: โœญ 65 (-89.81%)
Mutual labels:  state-management, state, store
snap-state
State management in a snap ๐Ÿ‘Œ
Stars: โœญ 23 (-96.39%)
Mutual labels:  state-management, preact, state
knockout-store
State management for Knockout apps.
Stars: โœญ 37 (-94.2%)
Mutual labels:  state-management, state, store
Westore
ๆ›ดๅฅฝ็š„ๅฐ็จ‹ๅบ้กน็›ฎๆžถๆž„
Stars: โœญ 3,897 (+510.82%)
Mutual labels:  state-management, state, store
stoxy
Stoxy is a state management API for all modern Web Technologies
Stars: โœญ 73 (-88.56%)
Mutual labels:  state-management, preact, state
vue
Vue integration for Nano Stores, a tiny state manager with many atomic tree-shakable stores
Stars: โœญ 25 (-96.08%)
Mutual labels:  state-management, state, store
Vue State Store
๐Ÿ“ฎ VSS (Vue State Store) - Vue State Management (with Publish & Subscribe pattern)
Stars: โœญ 128 (-79.94%)
Mutual labels:  state-management, state, store
Freactal
Clean and robust state management for React and React-like libs.
Stars: โœญ 1,676 (+162.7%)
Mutual labels:  state-management, preact, state
boutique
Immutable data storage
Stars: โœญ 44 (-93.1%)
Mutual labels:  state, store
mafuba
Simple state container for react apps.
Stars: โœญ 20 (-96.87%)
Mutual labels:  state, store
temperjs
State management for React, made simple.
Stars: โœญ 15 (-97.65%)
Mutual labels:  state-management, state
rex-state
Convert hooks into shared states between React components
Stars: โœญ 32 (-94.98%)
Mutual labels:  state-management, state
ReduxSimple
Simple Stupid Redux Store using Reactive Extensions
Stars: โœญ 119 (-81.35%)
Mutual labels:  state-management, state
vue-unstated
A tiny state management library for Vue Composition API.
Stars: โœญ 30 (-95.3%)
Mutual labels:  state-management, store
xstate
State machines and statecharts for the modern web.
Stars: โœญ 21,286 (+3236.36%)
Mutual labels:  state-management, state

Teaful

Teaful

Tiny, easy and powerful React state management library

npm version gzip size CI Status Maintenance Status Weekly downloads GitHub Discussions: Chat With Us PRs Welcome All Contributors

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 ๐Ÿ—บ

Installation ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป

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

import createStore from "teaful";

const { useStore } = createStore();

Or also with an initial store:

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

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

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<any> false Object with your initial store.
onAfterUpdate function false Function that is executed after each property change. More details.

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. 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. 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.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.cart.price(MyComponent)

How to export

We recommend using this type of export:

// โœ…
export const { useStore, getStore, withStore } = createStore({
  cart: { price: 0, items: [] },
});

This way you can import it with:

// โœ…
import { useStore } from '../store'

Avoid using a default export with all:

// โŒ
export default createStore({ cart: { price: 0, items: [] } });

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

// โŒ  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.

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 (
    <>
      <button onClick={() => setUsername("AnotherUserName")}>
        Update {username}
      </button>
      <button onClick={() => setCartPrice((v) => v + 1)}>
        Increment price: {cartPrice}โ‚ฌ
      </button>
    </>
  );
}

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

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

  return (
    <>
      <button
        onClick={() =>
          setStore((s) => ({
            ...s,
            username: "AnotherUserName",
          }))
        }
      >
        Update {store.username}
      </button>
      <button
        onClick={() =>
          setStore((s) => ({
            ...s,
            cart: { ...s.cart, price: s.cart.price + 1 },
          }))
        }
      >
        Increment price: {store.cart.price}โ‚ฌ
      </button>
    </>
  );
}

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:

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 (
    <>
      <button onClick={resetStore}>
        Reset store
      </button>
      <button onClick={resetCount}>
        Reset count
      </button>
      <button onClick={resetName}>
        Reset name
      </button>
    </>
  );
}

Another example:

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

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

  return Object.entries(formFields).map(([key, value]) => (
    <input 
      defaultValue={value} 
      type="text"
      key={key}
      onChange={e => {
        // 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:

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.

    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:
    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:

import { useState } from "react";

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

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

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 (
    <form onSubmit={saveIncreasedCount}>
      <input
        value={newCount}
        onChange={(e) => setNewCount(e.target.valueAsNumber)}
        type="number"
      />
      <button>Save the increased count value</button>
    </form>
  );
}

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:

const { withStore } = createStore();

class Counter extends Component {
  render() {
    const [count, setCount] = this.props.store;
    return (
      <div>
        <h1>{count}</h1>
        <button onClick={() => setCount((v) => v + 1)}>+</button>
        <button onClick={() => setCount((v) => v - 1)}>-</button>
        <button onClick={() => setCount(0)}>reset</button>
      </div>
    );
  }
}

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

Example with all store:

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

class Counter extends Component {
  render() {
    const [store, setStore] = this.props.store;
    return (
      <div>
        <h1>{store.count}</h1>
        <button onClick={() => setStore({ count: store.count + 1 })}>+</button>
        <button onClick={() => setStore({ count: store.count - 1 })}>-</button>
        <button onClick={() => setStore({ count: 0 })}>reset</button>
      </div>
    );
  }
}

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

    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.

    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 && <div className="erorMsg">{errorMsg}</div>}
          <div className="count">{count}</div>
          <button onClick={() => setCount((v) => v + 1)}>Increment</button>
        </>
      );
    }

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.

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

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

  return <div>Price: {price}</div>;
}

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.

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 <div>Price: {cart.price}</div>;
}

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

import createStore from "teaful";

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

Cart.js

import { useCart } from "./store";

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

Counter.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:

// ๐Ÿ˜ก
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:

// ๐Ÿฅต
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.

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:

// ๐Ÿคฉ
import { setStore } from "./store";

// ...
setStore({ 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:

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.

Teaful DevTools

Addons and extras ๐ŸŒ€

To facilitate the creation of libraries that extend Teaful (such as 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:

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:

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

render(<App />, document.getElementById('root'));

Examples ๐Ÿ–ฅ

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

Roadmap ๐Ÿ›ฃ

For 1.0:

  • React support
  • Teaful DevTools
  • TypeScript types support
  • 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):


Aral Roca Gomez

๐Ÿšง ๐Ÿ’ป

Danielo Artola

๐Ÿš‡ ๐Ÿ’ป

Yuki Shindo

๐Ÿš‡

YONGJAE LEE(์ด์šฉ์žฌ)

๐Ÿ›

niexq

๐Ÿ“– ๐Ÿš‡

nekonako

๐Ÿ“–

Shubham

๐Ÿ“–

Siddharth Borderwala

๐Ÿ“– ๐Ÿš‡ ๐Ÿ’ป

watcher

๐Ÿ’ป

This project follows the all-contributors specification. Contributions of any kind welcome!

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].