GithubHelp home page GithubHelp logo

tyin's Introduction

👔 Tyin

Typesafe state management in React for less!

✅ Tiny (<1K)
✅ Ergonomic
✅ Extensible

Tyin is pronounced tie-in: it ties a state into your app.

Installation

Use your favorite NPM package manager:

npm i tyin

Quickstart

Create the hook:

import storeHook from "tyin/hook";

export const useActivePage = storeHook(1);

Use it anywhere:

import { useActivePage } from "./hooks/useActivePage";

const Pagination = ({ maxPage }: PaginationProps) => {
  const activePage = useActivePage();

  return (
    <Container>
      <PreviousButton
        disabled={activePage === 1}
        onClick={() => useActivePage.set((page) => page - 1)}
      />
      <NextButton
        disabled={activePage === maxPage}
        onClick={() => useActivePage.set((page) => page + 1)}
      />
      <LastButton
        disabled={activePage === maxPage}
        onClick={() => useActivePage.set(maxPage)}
      />
    </Container>
  );
};

Real life applications are often more complex, though, so let's add the patch function from the object plugin to handle partial updates:

import storeHook from "tyin/hook";
import extend from "tyin/extend";
import objectAPI from "tyin/plugin-object";

const useUserState = extend(
  storeHook({
    name: "mausworks",
    roles: ["owner"],
  })
).with(objectAPI());

const UserNameInput = () => {
  const name = useUserState((user) => user.name);

  return (
    <TextInput
      value={name}
      onChange={({ target }) => useUserState.patch({ name: target.value })}
    />
  );
};

Tyin also ships with a convenience plugin for arrays—because not every state is an object!

In this example, we will add it, along with the persist plugin, and a custom setter called complete:

import storeHook from "tyin/hook";
import extend from "tyin/extend";
import arrayAPI from "tyin/plugin-array";
import persist from "tyin/plugin-persist";

const useTodoList = extend(
  storeHook([{
    task: "Walk the dog",
    completed: false
  }])
)
  // Add the array API:
  .with(arrayAPI())
  // Persist the state using the persist plugin:
  .with(persist({ name: "TodoList" }));
  // Add a custom setter:
  .with((store) => ({
    complete: (index: number) =>
      store.map((todo, i) =>
        i === index ? { ...todo, completed: true } : todo
      );
  }))
  // Remove the `with` (and `seal`) from the store:
  .seal();

const TodoApp = () => {
  const todos = useTodoList((todos) => todos.filter((todo) => !todo.completed));

  return (
    <Container>
      <TodoList
        todos={todos}
        onComplete={(index) => useTodoList.complete(index)}
      />
      <AddTodo onSubmit={(task) => useTodoList.push({ task, completed: false })} />
    </Container>
  );
};

We can also use Tyin outside of React:

import createStore from "tyin/store";
import extend from "tyin/extend";

const store = extend(createStore({ count: 1 })).with(...);

Project philosophy

These are the three tenets that allow for Tyin to be a fully featured state management solution in just a few bytes!

1. Modularity

Tyin doesn't come with a single entry point—that's intentional!

It instead ships a couple of highly standalone modules, so that the user can import only the functionality that they need.

2. Genericism

Tyin exposes generic APIs that aim to maximize ergonomics and minimize bundle size. Generic APIs facilitate code reuse, leading to synergies in consuming applications.

For example: There is no ObjectAPI.setKey(key, value) function, because ObjectAPI.patch({ [key]: value }) covers that need and a lot of other needs, simply by providing a generic API. This API is powerful enough to receive aggressive reuse in the consuming app; leading to an even smaller bundle size overall.

3. Composability

Tyin ships simple abstractions that favor composition over inheritance.

For example: Not every store needs a plugin, so the StoreAPI isn't readily extensible, that functionality is in extend instead.

Bundle size

To get an estimate on the bundle size you can run:

bun run src/test/size.ts

This is the current output:

export-all: 1619 bytes, 832 gzipped
export-common: 1309 bytes, 722 gzipped
hook: 529 bytes, 350 gzipped
plugin-persist: 415 bytes, 304 gzipped
plugin-array: 332 bytes, 190 gzipped
plugin-object: 286 bytes, 226 gzipped
store: 245 bytes, 212 gzipped
extend: 167 bytes, 138 gzipped

So, that means if you import everything; Tyin will add ~900 bytes to your bundle size, and the most minimal implementation (just tyin/hook) would only add ~350 bytes.

But this all depends on your bundler and configuration. In real-life scenarios it is often less. For dott.bio—using the export-object.js variant measured above—Tyin adds 550 bytes (according to next/bundle-analyzer).

Framework comparison

This table compares the "general usage" between Tyin, Zustand and Redux. I picked these frameworks, because I think most people are familiar with them.

Store setup Get state Set state
Tyin Create store, add plugins * Use store hook Call functions on the store
Zustand Create store, define setter functions on the state ** Use store hook Call setter functions on the state
Redux Create store, define setter actions, add provider to app Use useDispatch Dispatch setter actions with useDispatch

* = You rarely define your own setter functions when using Tyin. These are provided by plugins such as tyin/plugin-object instead.

** = This is technically not needed, but it is the recommended usage.

Project motivation

This project is inspired by zustand—I love zustand. I have, however, been "using it wrong" while working on dott.bio.

Most of the stores—after refactoring—now look like this:

const useTourState = create(() => ({ started: false, step: 0 }));

Something that you may find glaringly missing are any kind of state setters. The reason for this is that I have started to call setState directly on the hook instead:

useTourState.setState({ started: true });

This may look awkward or even wrong to some; not only because useTourState is a function itself, but also because useTourState.setState is like… super global—it lives "outside of react"… how can it even make the hook re-render?

It all works thanks to the useSyncExternalStore hook that ships with React. With it, you can make virtually anything re-render, and it is what drives both zustand and Tyin.

Zustand popularized the idea that "the hook is the store", and this project evolves on this idea even further. The key difference is that in Tyin, you put your state update functions on the store instead of the state. This separates your data from your code (which is generally considered good practice).

If you can look beyond "that initial irk", you may start seeing some benefits with using this pattern. Remember: you can now access and update the store from anywhere, and your components will simply comply—magic! 🪄

Another pain point I had with using zustand "the vanilla way" was that I kept declaring the same couple of state setters over and over again for each store. This is what finally drove me to just call setState directly instead since it's versatile enough for most use cases:

// Replace the state:
useTourState.setState({ started: true, step: 1 }, true);
// Partially apply the state:
useTourState.setState({ step: 2 });
// Accessing current state + partial application:
useTourState.setState((state) => ({ step: state.step + 1 }));

So, I realized that the functions I want for my stores are generic:

  • If my state is an object, I want to be able to replace, remove and add keys to it.
  • If my state is an array, I want to be able to push, filter, map, etc …
  • If a setter is more complex, I can put it in a hook (this can be advantageous anyways)

So why not replace custom state setters with generic ones?

At this point, I realized that zustand ships a lot of things that I have no interest in, so I wanted to make something simpler that only satisfies my requirements, and Tyin is the result!

tyin's People

Contributors

mausworks avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.