nanostores / nanostores

A tiny (286 bytes) state manager for React/RN/Preact/Vue/Svelte with many atomic tree-shakable stores
MIT License
5.32k stars 109 forks source link

Nano Stores 0.5 #57

Closed ai closed 3 years ago

ai commented 3 years ago

We collected first feedback of Nano Stores, and we are ready for next big refactoring and fix user’s complaints.

We will try to keep backward compatibility: we will wrap new methods to old methods and print warning.

Refactoring goals:

dotto.x by @Eddort solve most of these problems and can be a huge inspiration source (should we merge projects?).

Changes after release:

ai commented 3 years ago

Name fixing plan:

  1. Store → Atom
  2. Store definition → Store template
  3. Derived → Computed
  4. Remove create* and define* prefixes
ai commented 3 years ago

Solution for “People are not ready that some store can lost its value” problem:

  1. Stores always keep the value
  2. Stores have explicit default value in creator arguments
  3. Stores have listeners on& off for first listener subscribing and 1 second after last listener unsubscribing
ai commented 3 years ago

Questions:

  1. Should we add $ to store names? It will simplify map building and finding an argument name for $profile.listen(profile => …) callback.
  2. Should we add “action” concept? Now we have functions in store file without explicit name for this concept (and with mixing them with getters). Should we have some name convention for map parsing simplifying?
euaaaio commented 3 years ago

People are not ready that some store can lost its value

I like to know that the store is cleaning up after itself. Sometimes it can be useful. Since this is “nano” package, we can make something like “temp store”.

create and defined method prefix makes name too long

I like prefixes. They make API much more explicit. We can abbreviate all names to “a”, “b”, “c”. But why?

should we merge projects?

I made PR with Vue binding for dotto.x. It's interesting, but it has poor tests and it could be cleaner.

Store → Atom

😑

Should we add $ to store names?

No. I've been writing code with “$” in Vue 2 for several years. With the release of Vue 3, where “$” is no longer needed, I have become a little happier.

Should we add “action” concept?

No, it also seems to be an outdated and inconvenient feature. (Vue 2 flashbacks) Give an example, perhaps I misunderstood.

ai commented 3 years ago

I like to know that the store is cleaning up after itself. Sometimes it can be useful. Since this is “nano” package, we can make something like “temp store”.

My current idea is to make explicit cleaning in off event (last subscriber unsubscribed).

All our stores (Logux’s SyncMap, persistence store, router) will clean itself.

We can add autoclean(store) helper.

I like prefixes. They make API much more explicit.

I liked them too. But even in persistence, store methods became too long.

Svelte and Recoil use prefix-less names and API examples look cleaner.

But, Effector has prefixes.

@AleksandrSl what do you think?

Store → Atom 😑

We need some solution. You can suggest a better one with:

  1. Different name for atom and map
  2. Have common name of both atom and map (like store)
  3. Short and self-describable

No. I've been writing code with “$” in Vue 2 for several years. With the release of Vue 3, where “$” is no longer needed, I have become a little happier.

touchér

No, it also seems to be an outdated and inconvenient feature. (Vue 2 flashbacks). Give an example, perhaps I misunderstood.

It is just an optional syntax sugar:

Old:

function addUser (newUser) {
  if (validate(newUser)) {
    update(usersStore, users => users.concat([newUser]))
  }
}

New:

const addUser = action('addUser', newUser => {
  if (validate(newUser)) {
    update(usersStore, users => users.concat([newUser]))
  }
})

Action can be useful for better DevTools to show action names in events instead of store X was changed.

AleksandrSl commented 3 years ago

Answers:

Questions:

It will simplify map building

How $ will simplify building?

ai commented 3 years ago

How $ will simplify building?

Our JS files parser will automatically detect stores by simple variable name RegExp

eddort commented 3 years ago

Hello everyone! I would like to join the discussion with the following suggestions:

Lifecycle

This abstraction gives the ability to enhance your stores and keep them very small. I was able to reduce the size to 135B. With Lifecycle, we can write plugins like Persistent, Undo/Redo, CRDT sync, Loggers/Devtools, etc. Users can pick useful plugins for tasks and combine them for personal needs.

What is implemented in Dotto.x and what I would like to propose for implementation in Nanostores:

Deep Stores

This abstraction provides listening to pinpoint changes and set some values ​​by dots (like lodash). This is the reason why the state manager is named Dotto.x. “Dotto” is “dot” in Japanese.

Computed

It is the same as “derived” but resolves the diamond problem. The core of this solution is to listen to writable stores only. Here is my previous realization without “take” and “deep” operators. https://github.com/dottostack/dotto.x/blob/feat/effects/operators/computed/index.js#L42

Undo/Redo plugin

It is a very useful functionality and we can implement it with Lifecycle.

Persistent store as a plugin

This solution is simpler for testing than different types of stores because we can use all plugins in the integration layer and write tests only for stores.

euaaaio commented 3 years ago

I’m not a full-time developer, so I read the code more as a designer. Because of the long breaks between design and code, I often have to remember many things from scratch.

Actions and anything that helps make devtools and logging better is cool.

ai commented 3 years ago

Lifecycle

What is a lifecycle? Can you describe it a little more?

Deep Stores

Sure. What is requirements for that?

The core of this solution is to listen to writable stores only

Can we listen for other computed stores?

Undo/Redo plugin

Added to the list

Persistent store as a plugin

We have https://github.com/nanostores/persistent

What we can add there?

ai commented 3 years ago

I hear all reasons against atom, but for now, I think it is a better term:

  1. It is used in all other state managers: Recoil, Reatom, Jotai, etc.
  2. “Single” has many meaning too (singleton, single element of array).

Long names and prefixes are ok and even good. Readability is the ability to use the library and work with it without learning API.

I removed “create* removing” task from the list 👍

eddort commented 3 years ago

Can we listen for other computed stores?

https://github.com/nanostores/nanostores/pull/58

I'm going to answer other questions a little bit later

ai commented 3 years ago

A good reason for action(cb) wrapper for store changing functions: we can force using effect in action’s callback is async function (return Promise).

Or just wrap action callback to effect if it returns Promise.

eddort commented 3 years ago

What is a lifecycle? Can you describe it a little more?

Lifecycle is a module for wrapping the base store and providing these features:

Pluggable

In Nanostores we can use different types of stores. What if we would use Smart + Persistent? Or Smart + Persistent + Shared (with CRDT) at the same time and keep small size?

How it can look:

  1. Create a simple store without any additional functionality.
import { createStore } from "nanostores";
// step 1
// simple pure store
const projects = createStore({});
  1. Then we want to make this store smarter.
import { createStore } from "nanostores";
import { mount } from "@nanostores/plugins";
// step 1
// simple pure store
const projects = createStore({});
// step 2
// store with constructor and destructor
mount(projects, () => {
 projects.set({ name: "someProject" });
 return () => projects.set({});
});
  1. Then we would like to keep state in localstorage
import { createStore } from "nanostores";
import { mount, persistent } from "@nanostores/plugins";
// step 1
// simple pure store
const projects = createStore({});
// step 2
// store with constructor and destructor
mount(projects, () => {
 projects.set({ name: "someProject" });
 return () => projects.set({});
});
// step 3
// store keeping state
persistent(projects);
  1. Then add CRDT mechanism
import { createStore } from "nanostores";
import { mount, persistent, crdt } from "@nanostores/plugins";
// step 1
// simple pure store
const projects = createStore({});
// step 2
// store with constructor and destructor
mount(projects, () => {
 projects.set({ name: "someProject" });
 return () => projects.set({});
});
// step 3
// store keeps state
persistent(projects);
// step 4
// state will be synchronized
crdt(projects, someConfig);

This idea provides enhanced stores but keep threeshakable and friendly for all users because they can decide what they really want and get it. Integration of a new feature in the existing project requires 2 lines of code only.

Patch methods

In Nanostores we are patching base methods like the set in the Persistent store. But this is not scalable because if we start wrapping stores dynamically we can break chains of decoration. Dynamic wrapping is to wrap some method of stores by “dispose” (unsub) principle.

Shared data between calls.

It’s something like middleware in Redux with steroids. We can share data between all life cycles.

Community friendly

All users can write plugins for Nanostores and improve our ecosystem.

ai commented 3 years ago

Seems like we still need to remove create… prefix from names:

  1. createMapTemplate() looks too long. We will have many 3 words functions in API.
  2. Changing createMap name will help us with backward compatibility. map() will use new lifecycle, createMap() will use old auto-cleaning.

@Eddort I added an issue about lifecycle feature https://github.com/nanostores/nanostores/issues/59 Let’s discuss details there

ai commented 3 years ago

Seems like I will start DevTools from logger and in 0.5 release we will just have API to build DevTools extension.

I like the design of Effector Logger https://github.com/effector/logger

ai commented 3 years ago

What API we need for logger:

  1. Some way to register all stores. Global onCreate(({ store }) => …) event?
  2. Action fired with parameters
  3. Store changes with before→after state
  4. Change or set event was aborted
  5. Store got first listener
  6. Store lost last listener
  7. Store was removed from the memory (after timeout)

Events: 𝖓 action, 𝖓 change, 𝖓 abort, 𝖓 start, 𝖓 stop, 𝖓 clean (we discussed with @euaaaio to use white 𝖓 in block box as an icon).

What else?

droganov commented 3 years ago

If you still need opinions regarding the API, I find subscribe/listen misleading. To me it is an observable and I would propose observe and observeNow as a replacement

subscribe refers to pub-sub pattern, and feels like it is missing a channel.

Same for store.set it also has a key/value feeling, maybe store.next would work better.

ai commented 3 years ago

@droganov we need Svelte compatibility. This is why subscribe has this behavior. subscribe and set are part of Svelte store API.

droganov commented 3 years ago

| Derived → Computed

combineStores?

ai commented 3 years ago

combineStores

One source store is also a very popular use case for computed stores

droganov commented 3 years ago

One source store is also a very popular use case for computed stores

OK, I see... A kinda new concept, a store that includes some selector or reducer, looks like a projection, but it looks you can call set method.

ai commented 3 years ago

New lifecycle events system was moved from dotto.x by @Eddort https://github.com/nanostores/nanostores/pull/62

I cleaned up a little it https://github.com/nanostores/nanostores/pull/64

ai commented 3 years ago

Pure JS API to listen specific keys was added 7faec35

droganov commented 3 years ago
import { mount } from "@nanostores/plugins";

What if we name it lazy instead of mount? Mount creates expectation that a store will be mounted immediately after calling mount.

ai commented 3 years ago

@droganov we renamed mount to onMount(store, cb)

ai commented 3 years ago

I moved framework integrations to separated npm and GitHub projects in @nanostores org.

We finished main changes of 0.5. Now I will finish docs and @Eddort is planning to create logger to be sure that new API works fine.

ai commented 3 years ago

Router, Persistence Store and Logux Client were ported to Nano Stores 0.5 next and works great.

Seems like 0.5 is ready for release in next few days.

https://github.com/nanostores/nanostores/tree/next