https://github.com/e7h4n/ccstate Skip to content Navigation Menu Toggle navigation Sign in * Product + GitHub Copilot Write better code with AI + Security Find and fix vulnerabilities + Actions Automate any workflow + Codespaces Instant dev environments + Issues Plan and track work + Code Review Manage code changes + Discussions Collaborate outside of code + Code Search Find more, search less Explore + All features + Documentation + GitHub Skills + Blog * Solutions By company size + Enterprises + Small and medium teams + Startups By use case + DevSecOps + DevOps + CI/CD + View all use cases By industry + Healthcare + Financial services + Manufacturing + Government + View all industries View all solutions * Resources Topics + AI + DevOps + Security + Software Development + View all Explore + Learning Pathways + White papers, Ebooks, Webinars + Customer Stories + Partners + Executive Insights * Open Source + GitHub Sponsors Fund open source developers + The ReadME Project GitHub community articles Repositories + Topics + Trending + Collections * Enterprise + Enterprise platform AI-powered developer platform Available add-ons + Advanced Security Enterprise-grade security features + GitHub Copilot Enterprise-grade AI features + Premium Support Enterprise-grade 24/7 support * Pricing Search or jump to... Search code, repositories, users, issues, pull requests... Search [ ] Clear Search syntax tips Provide feedback We read every piece of feedback, and take your input very seriously. [ ] [ ] Include my email address so I can be contacted Cancel Submit feedback Saved searches Use saved searches to filter your results more quickly Name [ ] Query [ ] To see all available qualifiers, see our documentation. Cancel Create saved search Sign in Sign up Reseting focus You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session. You switched accounts on another tab or window. Reload to refresh your session. Dismiss alert {{ message }} e7h4n / ccstate Public * Notifications You must be signed in to change notification settings * Fork 2 * Star 12 License MIT license 12 stars 2 forks Branches Tags Activity Star Notifications You must be signed in to change notification settings * Code * Issues 0 * Pull requests 2 * Actions * Projects 0 * Security * Insights Additional navigation options * Code * Issues * Pull requests * Actions * Projects * Security * Insights e7h4n/ccstate main BranchesTags [ ] Go to file Code Folders and files Name Name Last commit Last commit message date Latest commit History 219 Commits .changeset .changeset .github .github .husky .husky docs docs packages packages .gitignore .gitignore .lintstagedrc.yaml .lintstagedrc.yaml .prettierignore .prettierignore .prettierrc .prettierrc LICENSE LICENSE README.md README.md commitlint.config.js commitlint.config.js eslint.config.js eslint.config.js package.json package.json pnpm-lock.yaml pnpm-lock.yaml pnpm-workspace.yaml pnpm-workspace.yaml screenshot.png screenshot.png tsconfig.json tsconfig.json tsconfig.options.json tsconfig.options.json vitest.config.js vitest.config.js vitest.workspace.json vitest.workspace.json View all files Repository files navigation * README * MIT license [397307204-590797c8-6edf-45cc-8eae] --------------------------------------------------------------------- Coverage Status NPM Type Definitions NPM Version npm package minimized gzipped size CI CodSpeed Badge License: MIT CCState is a semantic, strict, and flexible state management library suitable for medium to large single-page applications with complex state management needs. The name of CCState comes from three basic data types: computed, command, and state. Quick Features * Simple & Intuitive: Crystal-clear API design with just 3 data types and 2 operations * Rock-solid Reliability: Comprehensive test coverage reaching 100% branch coverage * Ultra-lightweight: Zero dependencies, only 500 lines of core code * Framework Agnostic: Seamlessly works with React, Vanilla JS, or any UI framework * Blazing Fast: Optimized performance from day one, 2x-7x faster than Jotai across scenarios Getting Started Installation # npm npm i ccstate # pnpm pnpm add ccstate # yarn yarn add ccstate Create Data Use state to store a simple value unit, and use computed to create a derived computation logic: // data.js import { state, computed } from 'ccstate'; export const userId$ = state(''); export const user$ = computed(async (get) => { const userId = get(userId$); if (!userId) return null; const resp = await fetch(`https://api.github.com/users/${userId}`); return resp.json(); }); Use data in React Use useGet and useSet hooks in React to get/set data, and use useResolved to get Promise value. // App.js import { useGet, useSet, useResolved } from 'ccstate'; import { userId$, user$ } from './data'; export default function App() { const userId = useGet(userId$); const setUserId = useSet(userId$); const user = useResolved(user$); return (
setUserId(e.target.value)} placeholder="github username" />
{user?.name} {user?.company}
); } Use createStore and StoreProvider to provide a CCState store to React, all states and computations will only affect this isolated store. // main.jsx import { createStore, StoreProvider } from 'ccstate'; import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import App from './App'; const rootElement = document.getElementById('root'); const root = createRoot(rootElement); const store = createStore(); root.render( , ); That's it! Click here to see the full example. Through these examples, you should have understood the basic usage of CCState. Next, you can read to learn about CCState's core APIs. Core APIs CCState provides several simple concepts to help developers better manage application states. And it can be used as an external store to drive UI frameworks like React. State State is the most basic value unit in CCState. A State can store any type of value, which can be accessed or modified through the store's get/set methods. Before explaining why it's designed this way, let's first look at the basic capabilities of State. import { store, state } from 'ccstate'; const store = createStore(); const userId$ = state(0); store.get(userId$); // 0 store.set(userId$, 100); store.get(userId$); // 100 const callback$ = state<(() => void) | undefined>(undefined); store.set(callback$, () => { console.log('awesome ccstate'); }); store.get(callback$)(); // console log 'awesome ccstate' These examples should be very easy to understand. You might notice a detail in the examples: all variables returned by state have a $ suffix. This is a naming convention used to distinguish an CCState data type from other regular types. CCState data types must be accessed through the store's get/set methods, and since it's common to convert an CCState data type to a regular type using get, the $ suffix helps avoid naming conflicts. Store In CCState, declaring a State doesn't mean the value will be stored within the State itself. In fact, a State acts like a key in a Map, and CCState needs to create a Map to store the corresponding value for each State - this Map is the Store. const count$ = state(0); // count$: { init: 0 } const store = createStore(); // imagine this as new Map() store.set(count$, 10); // simply imagine as map[count$] = 10 const otherStore = createStore(); // another new Map() otherStore.get(count$); // anotherMap[$count] ?? $count.init, returns 0 This should be easy to understand. If Store only needed to support State types, a simple Map would be sufficient. However, CCState needs to support two additional data types. Next, let's introduce Computed, CCState's reactive computation unit. Computed Computed is CCState's reactive computation unit. You can write derived computation logic in Computed, such as sending HTTP requests, data transformation, data aggregation, etc. import { computed, createStore } from 'ccstate'; const userId$ = state(0); const user$ = computed(async (get) => { const userId = get(userId$); const resp = await fetch('/api/users/' + userId); return resp.json(); }); const store = createStore(); const user = await store.get(user$); Does this example seem less intuitive than State? Here's a mental model that might help you better understand what's happening: * computed(fn) returns an object {read: fn}, which is assigned to user$ * When store.get(user$) encounters an object which has a read function, it calls that function: user$.read(store.get) This way, Computed receives a get accessor that can access other data in the store. This get accessor is similar to store.get and can be used to read both State and Computed. The reason CCState specifically passes a get method to Computed, rather than allowing direct access to the store within Computed, is to shield the logic within Computed from other store methods like store.set. The key characteristic of Computed is that it can only read states from the store but cannot modify them. In other words, Computed is side-effect free. In most cases, side-effect free computation logic is extremely useful. They can be executed any number of times and have few requirements regarding execution timing. Computed is one of the most powerful features in CCState, and you should try to write your logic as Computed whenever possible, unless you need to perform set operations on the Store. Command Command is CCState's logic unit for organizing side effects. It has both set and get accessors from the store, allowing it to not only read other data types but also modify State or call other Command. import { command, createStore } from 'ccstate'; const user$ = state(undefined); const updateUser$ = command(async ({ set }, userId) => { const user = await fetch('/api/users/' + userId).then((resp) => resp.json()); set(user$, user); }); const store = createStore(); store.set(updateUser$, 10); // fetchUserInfo(userId=10) and set to user$ Similarly, we can imagine the set operation like this: * command(fn) returns an object {write: fn} which is assigned to updateUser$ * When store.set(updateUser$) encounters an object which has a write function, it calls that function: updateUser$.write({set: store.set, get: store.get}, userId) Since Command can call the set method, it produces side effects on the Store. Therefore, its execution timing must be explicitly specified through one of these ways: * Calling a Command through store.set * Being called by the set method within other Commands * Being triggered by subscription relationships established through store.sub Subscribing to Changes CCState provides a sub method on the store to establish subscription relationships. import { createStore, state, computed, command } from 'ccstate'; const base$ = state(0); const double$ = computed((get) => get(base$) * 2); const store = createStore(); store.sub( double$, command(({ get }) => { console.log('double', get(double$)); }), ); store.set(base$, 10); // will log to console 'double 20' There are two ways to unsubscribe: 1. Using the unsub function returned by store.sub 2. Using an AbortSignal to control the subscription The sub method is powerful but should be used carefully. In most cases, Computed is a better choice than sub because Computed doesn't generate new set operations. // use sub const user$ = state(undefined); const userId$ = state(0); store.sub( userId$, command(({ set, get }) => { const userId = get(userId$); const user = fetch('/api/users/' + userId).then((resp) => resp.json()); set(user$, user); }), ); // use computed const userId$ = state(0); const user$ = computed(async (get) => { return await fetch('/api/users/' + get(userId$)).then((resp) => resp.json()); }); Using Computed to write reactive logic has several advantages: * No need to manage unsubscription * No need to worry about it modifying other States or calling other Command Here's a simple rule of thumb: if some logic can be written as a Computed, it should be written as a Computed. Comprasion Type get set sub target as sub callback State Computed Command That's it! Next, you can learn how to use CCState in React. Using in React To begin using CCState in a React application, you must utilize the StoreProvider to provide a store for the hooks. // main.tsx import { createStore, StoreProvider } from 'ccstate'; import { App } from './App'; import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; const store = createStore(); createRoot(document.getElementById('root')).render( , ); All descendant components within the StoreProvider will use the provided store as the caller for get and set operations. You can place the StoreProvider inside or outside of StrictMode; the functionality is the same. Retrieving Values The most basic usage is to use useGet to retrieve the value from State or Computed. // data/count.ts import { state } from 'ccstate'; export const count$ = state(0); // App.tsx import { useGet } from 'ccstate'; import { count$ } from './data/count'; function App() { const count = useGet(count$); return
{count}
; } useGet returns a State or a Computed value, and when the value changes, useGet triggers a re-render of the component. useGet does not do anything special with Promise values. In fact, useGet is equivalent to a single store.get call, plus a store.sub to ensure reactive updates to the React component. Two other useful hooks are available when dealing with Promise values. First, we introduce useLoadable. // data/user.ts import { computed } from 'ccstate'; export const user$ = computed(async () => { return fetch('/api/users/current').then((res) => res.json()); }); // App.tsx import { useLoadable } from 'ccstate'; import { user$ } from './data/user'; function App() { const user_ = useLoadable(user$); if (user_.state === 'loading') return
Loading...
; if (user_.state === 'error') return
Error: {user_.error.message}
; return
{user_.data.name}
; } useLoadable accepts Value/Computed that returns a Promise and wraps the result in a Loadable structure. type Loadable = | { state: 'loading'; } | { state: 'hasData'; data: T; } | { state: 'hasError'; error: unknown; }; This allows you to render loading and error states in JSX based on the state. useLoadable suppresses exceptions, so it will not trigger an ErrorBoundary. Another useful hook is useResolved, which always returns the resolved value of a Promise. // App.tsx import { useResolved } from 'ccstate'; import { user$ } from './data/user'; function App() { const user = useResolved(user$); return
{user?.name}
; } useResolved only returns the parameter passed to the resolve function so that it will return undefined during loading and when encountering error values. Like useLoadable, useResolved also suppresses exceptions. In fact, useResolved is a simple wrapper around useLoadable. // useResolved.ts import { useLoadable } from './useLoadable'; import type { Computed, State } from '../core'; export function useResolved(atom: State> | Computed>): T | undefined { const loadable = useLoadable(atom); return loadable.state === 'hasData' ? loadable.data : undefined; } useLastLoadable & useLastResolved In some scenarios, we want a refreshable Promise Computed to maintain its previous result during the refresh process instead of showing a loading state. CCState provides useLastLoadable and useLastResolved to achieve this functionality. import { useLoadable } from 'ccstate'; import { user$ } from './data/user'; function App() { const user_ = useLastLoadable(user$); // Keep the previous result during new user$ request, without triggering loading state if (user_.state === 'loading') return
Loading...
; if (user_.state === 'error') return
Error: {user_.error.message}
; return
{user_.data.name}
; } useLastResolved behaves similarly - it always returns the last resolved value from a Promise Atom and won't reset to undefined when a new Promise is generated. Updating State / Triggering Command The useSet hook can be used to update the value of State, or trigger Command. It returns a function equivalent to store.set when called. // App.tsx import { useSet } from 'ccstate'; import { count$ } from './data/count'; function App() { const setCount = useSet(count$); // setCount(x => x + 1) is equivalent to store.set(count$, x => x + 1) return ; } Testing & Debugging Testing Value/Computed should be as simple as testing a Map. // counter.test.ts import { test } from 'vitest'; import { createStore, state } from 'ccstate'; test('test counter', () => { const store = createStore(); const count$ = state(0); store.set(count$, 10); expect(store.get(count$)).toBe(10); }); Here are some tips to help you better debug during testing. ConsoleInterceptor Use ConsoleInterceptor to log most store behaviors to the console during testing: import { createConsoleDebugStore, state, computed, command } from 'ccstate'; const base$ = state(1, { debugLabel: 'base$' }); const derived$ = computed((get) => get(base$) * 2); const store = createConsoleDebugStore([base$, 'derived'], ['set', 'sub']); // log sub & set actions store.set(base$, 1); // console: SET [V0:base$] 1 store.sub( derived$, command(() => void 0), ); // console: SUB [V0:derived$] Concept behind CCState CCState is inspired by Jotai. While Jotai is a great state management solution that has benefited the Motiff project significantly, as our project grew larger, especially with the increasing number of states (10k~100k atoms), we felt that some of Jotai's design choices needed adjustments, mainly in these aspects: * Too many combinations of atom init/setter/getter methods, need simplification to reduce team's mental overhead * Should reduce reactive capabilities, especially the onMount capability - the framework shouldn't provide this ability * Some implicit magic operations, especially Promise wrapping, make the application execution process less transparent To address these issues, I created CCState to express my thoughts on state management. Before detailing the differences from Jotai, we need to understand CCState's data types and subscription system. More semantic data types Like Jotai, CCState is also an Atom State solution. However, unlike Jotai, CCState doesn't expose Raw Atom, instead dividing Atoms into three types: * State (equivalent to "Primitive Atom" in Jotai): State is a readable and writable "variable", similar to a Primitive Atom in Jotai. Reading a State involves no computation process, and writing to a State just like a map.set. * Computed (equivalent to "Read-only Atom" in Jotai): Computed is a readable computed variable whose calculation process should be side-effect free. As long as its dependent Atoms don't change, repeatedly reading the value of a Computed should yield identical results. Computed is similar to a Read-only Atom in Jotai. * Command (equivalent to "Write-only Atom" in Jotai): Command is used to encapsulate a process code block. The code inside an Command only executes when an external set call is made on it. Command is also the only type in ccstate that can modify value without relying on a store. Subscription System CCState's subscription system is different from Jotai's. First, CCState's subscription callback must be an Command. export const userId$ = state(1); export const userIdChange$ = command(({ get, set }) => { const userId = get(userId$); // ... }); // ... import { userId$, userIdChange$ } from './data'; function setupPage() { const store = createStore(); // ... store.sub(userId$, userIdChange$); // ... } The consideration here is to avoid having callbacks depend on the Store object, which was a key design consideration when creating CCState. In CCState, sub is the only API with reactive capabilities, and CCState reduces the complexity of reactive computations by limiting Store usage. CCState does not have APIs like onMount. This is because CCState considers onMount to be fundamentally an effect, and providing APIs like onMount in computed would make the computation process non-idempotent. Avoid useEffect in React While Reactive Programming like useEffect has natural advantages in decoupling View Components, it causes many complications for editor applications like Motiff. Regardless of the original design semantics of useEffect, in the current environment, useEffect's semantics are deeply bound to React's rendering behavior. When engineers use useEffect, they subconsciously think "callback me when these things change", especially "callback me when some async process is done". While it's easy to write such waiting code using async/await, it feels unnatural in React. // App.jsx // Reactive Programming in React export function App() { const userId = useUserId(); // an common hook to takeout userId from current location search params const [user, setUser] = useState(); const [loading, setLoading] = useState(); useEffect(() => { setLoading(true); fetch('/api/users/' + userId) .then((resp) => resp.json()) .then((u) => { setLoading(false); setUser(u); }); }, [userId]); if (loading) { return
Loading...
; } return <>{user?.name}; } When designing CCState, we wanted the trigger points for value changes to be completely detached from React's Mount/Unmount lifecycle and completely decoupled from React's rendering behavior. // data.js export const userId$ = state(0) export const init$ = command(({set}) => { const userId = // ... parse userId from location search set(userId$, userId) }) export const user$ = computed(get => { const userId = get(userId$) return fetch('/api/users/' + userId).then(resp => resp.json()) }) // App.jsx export function App() { const user = useLastResolved(user$); return <>{user?.name}; } // main.jsx const store = createStore(); store.set(init$) const rootElement = document.getElementById('root')!; const root = createRoot(rootElement); root.render( , ); Changelog & TODO Changelog Here are some new ideas: * Integration with svelte / solid.js * Enhance devtools + Support viewing current subscription graph and related atom values + Enable logging and breakpoints for specific atoms in devtools * Performance improvements + Mount atomState directly on atoms when there's only one store in the application to reduce WeakMap lookup overhead + Support static declaration of upstream dependencies for Computed to improve performance by disabling runtime dependency analysis Contributing CCState welcomes any suggestions and Pull Requests. If you're interested in improving CCState, here are some basic steps to help you set up a CCState development environment. pnpm install pnpm husky # setup commit hooks to verify commit pnpm vitest # to run all tests pnpm lint # check code style & typing Special Thanks Thanks Jotai for the inspiration and some code snippets, especially the test cases. Without their work, this project would not exist. License This project is licensed under the MIT License - see the LICENSE file for details. About No description, website, or topics provided. Resources Readme License MIT license Activity Stars 12 stars Watchers 1 watching Forks 2 forks Report repository Releases No releases published Packages 0 No packages published Contributors 3 * * * Languages * TypeScript 96.5% * JavaScript 2.8% * Other 0.7% Footer (c) 2024 GitHub, Inc. Footer navigation * Terms * Privacy * Security * Status * Docs * Contact * Manage cookies * Do not share my personal information You can't perform that action at this time.