{"id":15391382,"url":"https://github.com/sultan99/holycow","last_synced_at":"2025-04-09T23:18:59.189Z","repository":{"id":62305840,"uuid":"375186938","full_name":"sultan99/holycow","owner":"sultan99","description":"🐮 React hook-based state management","archived":false,"fork":false,"pushed_at":"2025-01-30T09:25:11.000Z","size":16317,"stargazers_count":26,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-09T23:18:56.376Z","etag":null,"topics":["fp","hook","javascript","react","state-management"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sultan99.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-06-09T01:09:01.000Z","updated_at":"2025-04-09T14:38:57.000Z","dependencies_parsed_at":"2024-10-01T15:11:02.866Z","dependency_job_id":"36542fab-3267-490c-be32-8f2cfb0b75c6","html_url":"https://github.com/sultan99/holycow","commit_stats":{"total_commits":32,"total_committers":1,"mean_commits":32.0,"dds":0.0,"last_synced_commit":"a9938e21feae4cfdbf7560ac94715f12f068261a"},"previous_names":[],"tags_count":16,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sultan99%2Fholycow","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sultan99%2Fholycow/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sultan99%2Fholycow/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sultan99%2Fholycow/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sultan99","download_url":"https://codeload.github.com/sultan99/holycow/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248125643,"owners_count":21051777,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["fp","hook","javascript","react","state-management"],"created_at":"2024-10-01T15:10:55.020Z","updated_at":"2025-04-09T23:18:59.165Z","avatar_url":"https://github.com/sultan99.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Holy State\n[![codecov](https://codecov.io/gh/sultan99/holycow/branch/main/graph/badge.svg)](https://codecov.io/gh/sultan99/holycow)\n![npm bundle size](https://img.shields.io/bundlephobia/minzip/%40holycow%2Fstate?label=gzip%20size\u0026color=green\u0026link=https%3A%2F%2Fbundlephobia.com%2Fpackage%2F%40holycow%2Fstate%401.2.0)\n![NPM Downloads](https://img.shields.io/npm/d18m/%40holycow%2Fstate?link=https%3A%2F%2Fnpmtrends.com%2F%40holycow%2Fstate)\n![GitHub License](https://img.shields.io/github/license/sultan99/holycow?link=https%3A%2F%2Fgithub.com%2Fsultan99%2Fholycow%2Fblob%2Fmain%2FLICENSE)\n\n\u003e Hook-based state management library for [React](https://github.com/facebook/react) applications.\n\n\u003cbr/\u003e\n\u003cbr/\u003e\n\u003cdiv align=\"center\"\u003e\n  \u003cimg src=\"./holy-cow.gif\" width=\"160px\"\u003e\n\u003c/div\u003e\n\u003cbr/\u003e\n\u003cbr/\u003e\n\u003cbr/\u003e\n\n## Quick intro\nHoly moly, you are here! You're more than welcome!\n\nSo, it is all about state management handled by hooks. Think of it as a utility for creating hooks that can store a global state across the entire application. The coolest part is that it works without context providers, observables, selectors, or HOC connectors. No boilerplate code but hooks.\n\n### 🦄 Main features\n- The library is tree-shakeable with no external dependency. [Gzip size: ~1.9kb](https://bundlephobia.com/package/@holycow/state@1.2.0). \n- The state hooks can be used outside of the React tree.\n- Greedy rendering. Only updated values trigger component rendering.\n- Computed values with caching and hook nesting.\n- Asynchronous actions.\n- Subscription to the state changes.\n- Event-driven architecture support.\n- Friendly with functional programming.\n- Strongly typed with TypeScript.\n\n\u003cbr/\u003e\n\u003cdiv align=\"center\"\u003e\n  \u003cimg src=\"./wrong-marketing.gif\"\u003e\n\u003c/div\u003e\n\u003cbr/\u003e\n\n### 🚀 Getting started\n- 📖 [Online documentation](https://holy-cow.gitbook.io/holy-state/)\n- 🍿 [Online demo](https://codesandbox.io/s/github/sultan99/cards/tree/main)\n- 🐙 [Demo code source](https://github.com/sultan99/cards)\n\n```sh\nnpm install @holycow/state\n```\n\n```jsx\nimport {createState} from '@holycow/state'\n\n// 👇 your store is a hook!\nconst useUser = createState({\n  id: 1,\n  name: 'Homer Simpson',\n  address: {\n    house: 742,\n    street: 'Evergreen Terrace',\n  },\n})\n\nconst UserName = () =\u003e {\n  const {name} = useUser() // 👈 value from the state\n  return \u003cdiv\u003e{name}\u003c/div\u003e\n}\n\nconst {id, name, address} = useUser \n// any values 👆 from the hook can be used outside of components\n```\n\n### 🍃 State update\nIt is quite simple to modify state values with the built-in `set` function.\n\n```js\nconst {set} = useUser()\n\nset('name', 'Ovuvuevuevue') // key \u0026 value\nset('id',  prevId =\u003e prevId + 1) // key \u0026 function\nset('address.street', 'rue de Beggen') // path \u0026 value, no spreading objects\n\n// atomic updates: multiple values at the same time\nset(state =\u003e ({\n  ...state,\n  id: state.id + 1,\n  name: 'Ovuvuevuevue',\n}))\n// or object as updating input\nset({\n  id: prevId =\u003e prevId + 1,\n  name: 'Ovuvuevuevue',\n})\n```\n\nThe `set` function is not only overloaded but curried as well. We can apply parameters to it partially one by one:\n\n```js\nconst setId = set('id') // returns function which will update 'id'\nsetId(2) // actual updates with a value\nsetId(prevId =\u003e prevId + 1) // or using function\n\nfetch('/api/users/1/address')\n  .then(res =\u003e res.json())\n  .then(set('address')) // equals 👉 .then(data =\u003e set('address', data))\n```\n\n### 🎬 Actions\nAn action is any piece of code that modifies the state and targets some specific task, unlike the `set` function, which is more generic and used for a simple value update. It is a good place to move business logic like validation or network operations. \n\nAction expects a curried function as a parameter. The first function provides the state — the second one handles the action payload.\n\n```jsx\nimport {createState, action} from '@holycow/state'\n\nconst useAuth = createState({\n  token: '',\n  error: '',\n  loading: false, // 👇 state         👇 payload\n  login: action(({set, loading}) =\u003e formData =\u003e {\n    if (loading) return\n    set('error', '') // 👈 the state can be updated directly from the action\n    set('loading', true)\n    fetch('/api/login', {method: 'POST', body: formData})\n      .then(res =\u003e res.json())\n      .then(set('token'))\n      .catch(set('error'))\n      .finally(() =\u003e set('loading', false))\n  }),\n})\n\n// ⚡ handler is created outside of the component, +1 performance\nconst handleSubmit = event =\u003e {\n  event.preventDefault()\n  useAuth.login(new FormData(event.target))\n}\n\nconst Login = () =\u003e {\n  const {loading, error} = useAuth()\n  return (\n    \u003cform onSubmit={handleSubmit}\u003e\n      \u003cinput name='email' type='text'/\u003e\n      \u003cinput name='password' type='password'/\u003e\n      \u003cp\u003e{error}\u003c/p\u003e\n\n      \u003cbutton type='submit' disabled={loading}\u003e\n        {loading ? 'Submitting' : 'Login'}\n      \u003c/button\u003e\n    \u003c/form\u003e\n  )\n}\n```\n\n### 🧠 Smart rendering\nUnlike other state management, the holy state library does not require memoized selectors or further optimization to avoid unnecessary rerenders. Instead, it comes with a state tracking feature out of the box. Only components with altered values get rerendered.\n\n```jsx\nconst Street = () =\u003e {\n  const {address} = useUser() \n  return \u003cdiv\u003e{address.street}\u003c/div\u003e // Evergreen Terrace\n}\n\n// 💤 no render even the address object was updated, +1 performance\nuseUser.set('address.house', 10)\n// 💤 no render, equal value applied, +1 performance\nuseUser.set('address.street', 'Evergreen Terrace')\n// 🏃‍♂️ now it will be rendered\nuseUser.set('address.street', 'Spooner')\n```\n\n### 🧮 Computed values\nA computed value is a value returned by a specified function. The function's input can be a state value or any other hook. To avoid unnecessary computations, the computed value is cached and recomputed only when the current dependency has changed. Conceptually, computed values are similar to spreadsheets' formulas or Redux memoized selectors.\n\n```jsx\nimport {createState, computed} from '@holycow/state'\n\nconst useUser = createState({\n  name: 'Peter',\n  birthDate: {\n    day: 8,\n    month: 12,\n    year: 1979,\n  },\n  // 🦥 lazy evaluation, function will be called when the value is used\n  age: computed(state =\u003e\n    new Date().getFullYear() - state.birthDate.year\n  ), \n})\n\n// usage\nconst UserAge = () =\u003e {\n  const {name, age} = useUser()\n  // here 'age' 👆 value is calculated and cached\n  return \u003cdiv\u003e{name} is {age} years old guy.\u003c/div\u003e\n}\n\nconst homerAge = userUser.age // 👈 value from the cache, +1 performance\n```\n\nIn the example above, the computed value age will be recalculated if the year of birthDate is modified. Otherwise, it will use the cached value.\n\nWhat if we want to keep our `age` value updated when the year is changed? Let's assume our hard-working QA engineer opens our app on 31 December at 11:58! \n\nWe could use the  [useCurrentYear](https://gist.github.com/sultan99/8ad653259263e052951f9d961d5d982e) hook to keep the value updated, respectively. Then we should wrap the hook with the side effect function.\n\n```js\nconst useUser = createState({\n  name: 'Peter',\n  birthDate: {\n    day: 8,\n    month: 12,\n    year: 1979,\n  }, // side effect function 👇\n  age: computed((state, sideEffect) =\u003e\n    sideEffect(useCurrentYear) - state.birthDate.year\n    // when it's required to pass a parameter 👇\n    // sideEffect(() =\u003e useCurrentYear('some-params'))\n  ),\n})\n```\n\nWe should remember by using side effects, we lose the benefit of caching.\n\n### 🤹 Selectors\nSelectors are designed for convenient state access. The selector retrieves a value from the state at a given path. If we query more than one value, the selector will return an array with the requested values.\n\n```jsx\nconst useMessages = createState({\n  author: {\n    id: 1,\n    name: 'Peter',\n  },\n  messages: [\n    {id: 10, text: 'Hello'},\n    {id: 20, text: 'World!'},\n  ]\n})\n\n// single value\nconst authorName = useMessages('author.name')\n// 👆 equivalent 👇\nconst {author} = useMessages()\nconst authorName = author.name\n\n// multiple values\nconst [authorName, firstMessage] = useMessages('author.name', 'messages.0.text')\n// 👆 equivalent 👇\nconst {author, messages} = useMessages()\nconst authorName = author.name\nconst firstMessage = messages[0].text\n\n// one line component with a selector 👇\nconst AuthorName = () =\u003e \u003cdiv\u003e{useMessages('author.name')}\u003c/div\u003e\n```\n\nThe same trick we can do with actions or `set` functions:\n```js\nconst setUser = useUser('set')\nconst setMessage = useMessages('set')\n// 👆 equivalent 👇\nconst {set: setUser} = useUser()\nconst {set: setMessage} = useMessages()\n```\n\n### 🗃️ Context state\nContext state is a multiple-instance state with its own scope. It is designed to create reusable nested components that share one state with their child components. It is similar to the React Context, but powered by holy state features.\n\n- Greedy rendering, meaning that only updated values trigger component rendering.\n- Computed values with caching and hook nesting.\n- Asynchronous actions.\n\nHowever, the state hook can only be used inside React components and does not support subscription to the state changes.\n\nThe `createContextState` function creates a state and returns a tuple with a context provider and a hook. The initial context state can be overridden by a parent component. This allows you to create uncontrolled, controlled, or partially controlled components.\n\n```jsx\nimport {action, createContextState} from '@holycow/state'\n\nconst [Context, useCounter] = createContextState({\n  // 👇 the context initial state\n  count: 0,\n  name: `Untitled`,\n  increment: action(({set}) =\u003e () =\u003e \n    set(`count`, value =\u003e value + 1)\n  ),\n})\n\nconst Label = () =\u003e {\n  // same as the holy state hook 👇\n  const {name, count} = useCounter()\n  return \u003ch1\u003e{name}: {count}\u003c/h1\u003e\n}\n\nconst Button = () =\u003e {\n  const {increment} = useCounter()\n  return (\n    \u003cbutton onClick={increment}\u003e\n      Increment\n    \u003c/button\u003e\n  )\n}\n\n// only assigned props {name, count, increment}\n// will override the initial context state\n// undefined props 👇 will be ignored\nconst Counter = props =\u003e (\n  \u003cContext value={props}\u003e\n    \u003cLabel/\u003e\n    \u003cButton/\u003e\n  \u003c/Context\u003e\n)\n\nconst App = () =\u003e {\n  const [age, setAge] = useState(21)\n  return (\n    \u003cmain\u003e\n      // each Counter component will have it is own state\n      \u003cCounter name='🐑 counter'/\u003e\n      \u003cCounter count={age} increment={() =\u003e setAge(age + 1)}/\u003e\n      // 👆 controlled component by parent component\n    \u003c/main\u003e\n  )\n}\n```\n\n### 📬 State subscriptions\nWe can subscribe to the state changes and get notified when the state is updated. The `subscribe` function accepts a callback function as a parameter and returns another function for unsubscription.\n\n```js\n// subscription to the whole state     👇\nconst unsubscribe = useUser.subscribe(state =\u003e {\n  localStorage.setItem('user', JSON.stringify(state))\n})\n\nunsubscribe() // canceling subscription 👆\n\n// subscription to specific 👇 value\nuseUser.subscribe('address.street', street =\u003e {\n  console.log(`User street was changed to ${street}`)\n})\n```\n\n### 📢 Signal Events\nSignals provide a simple way to communicate between decoupled hooks that don't know each other directly, but some of them wait for the other to occur to do something. So, for example, we could import a user profile state in lazy mode when the user gets logged in. But before that, we fetch only the required hooks to handle the guest state. On the other hand, it avoids tight coupling of hooks and can resolve circle dependencies issues. Shortly, the signals are the implementation of event-driven architecture.\n\n\u003e It is optional to use the signals. Subscriptions and nesting hooks can provide the same functionality.\n\nThere are three steps to use the signals:\n - Creation of the emitter function: `const ringDoorbell = createSignal()`.\n - Creation of the signal listener: `on(ringDoorbell, useDoor.open)`.\n - Call `ringDoorbell()` function to trigger the action.\n\n```js\nimport {createSignal, on} from '@holycow/state'\n\nconst logout = createSignal() // 👈 creates logout signal function\n\n// auth.js\non(logout, () =\u003e { // 👈 listens to the logout signal\n  useAuth.logout()\n  console.log('Bye bye!')\n})\n\n// user.js\non(logout, () =\u003e {\n  useUser.reset() // built-in function that restore initial state of the hook\n  localStorage.removeItem('user')\n})\n\n// 👇 emits the logout signal\nlogout()\n```\n\nTo disable the listener, we should call the function returned by the `on` function.\n\n```js\nconst off = on(login, useAuth.login)\n\nlogin('homer@simpson.com', 'pa$$word')\n\noff() // 👈 stops to react on the login signal\n```\n\nSignals can be executed once and then removed from the listeners.\n\n```js\nimport {createSignal, once} from '@holycow/state'\n\nconst init = createSignal()\n\n// 👇 instead of 'on' we use 'once'\nonce(init, () =\u003e {\n  usePosts.loadPosts()\n})\n\ninit() // 👈 will trigger the callback function\ninit() // no effects\n```\n\n### 📎 TypeScript\nThe state typing is designed to be seamless. Once it is typed, it should provide the correct types everywhere.\n\n```tsx\nimport type {Action, Computed, Computed} from '@holycow/state'\nimport {createState} from '@holycow/state'\n\ntype Todo = {\n  id: number\n  checked: boolean\n  description: string\n}\n/**\n * Computed\u003cStateType, ReturnType\u003e\n * Action\u003cStateType\u003e action with no payload\n * Action\u003cStateType, [PayloadType1, PayloadType2 ...PayloadTypeN]\u003e\n */\ntype TodosState = {\n  filter: 'all' | 'completed' | 'uncompleted'\n  todos: Todo[]\n  filteredTodos: Computed\u003cTodosState, Todo[]\u003e // 👈 computing function returns Todo[]\n  addTodo: Action\u003cTodosState, [string, boolean | undefined]\u003e // 👈 action with payloads\n  clearTodos: Action\u003cTodosState\u003e // 👈 action without payload\n}\n// 👆 TypeScript zone, it can even be in a separate file.\nconst useTodos = createState\u003cTodosState\u003e({\n// 👇 below like a normal JS code\n  filter: 'all',\n  todos: [\n    {id: 1, checked: true, description: 'Buy milk'},\n    {id: 2, checked: false, description: 'Clean room'},\n  ],\n  filteredTodos: computed(state =\u003e {\n    const {filter, todos} = state \n    const isAll = filter === 'all'\n    const isCompleted = filter === 'completed'\n  \n    return isAll ? todos : todos.filter(\n      ({checked}) =\u003e checked === isCompleted\n    )\n  }),\n  addTodo: action(state =\u003e (description, checked = false) =\u003e {\n    const {set, todos} = state\n    const id = todos.reduce((acc, {id}) =\u003e Math.max(id + 1, acc), 0)\n    const newTodo = {id, description, checked}\n\n    set('todos', [...todos, newTodo])\n  }),\n  clearTodos: action(({set}) =\u003e () =\u003e {\n    set('todos', [])\n  })\n})\n\nconst [addTodo, set, todo] = useTodos('addTodo', 'set', 'todos.0')  // ✅ all good\nconst [addTodo, set, todo] = useTodos('addtodo', 'set', 'todos.0')  // ❌ type error\n                                      // 👆 typos\n\ntodo?.description // ✅ all good\ntodo.description  // ❌ type error, it might be undefined\n\naddTodo('Buy milk') // ✅ all good\naddTodo(123) // ❌ type error\n\nset('filter', 'completed') // ✅ all good\nset('filter', 'new') // ❌ type error\n\nconst setFilter = set('filter') // curried function\nsetFilter('completed') // ✅ all good\nsetFilter('new') // ❌ type error\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsultan99%2Fholycow","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsultan99%2Fholycow","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsultan99%2Fholycow/lists"}