{"id":18427025,"url":"https://github.com/kilianc/mozzarella","last_synced_at":"2025-08-24T06:09:33.961Z","repository":{"id":39204181,"uuid":"257423969","full_name":"kilianc/mozzarella","owner":"kilianc","description":"🇮🇹 A cheezy-simple hook based immutable store","archived":false,"fork":false,"pushed_at":"2024-10-01T14:02:56.000Z","size":1774,"stargazers_count":6,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2024-10-18T08:35:53.394Z","etag":null,"topics":["hooks","immer","react","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kilianc.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/funding.yml","license":null,"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},"funding":{"custom":"https://www.paypal.me/kilianciuffolo"}},"created_at":"2020-04-20T23:01:40.000Z","updated_at":"2024-10-01T14:02:59.000Z","dependencies_parsed_at":"2023-10-01T14:53:08.513Z","dependency_job_id":"d42b3573-97ad-4936-9251-9f533d122ee8","html_url":"https://github.com/kilianc/mozzarella","commit_stats":{"total_commits":277,"total_committers":4,"mean_commits":69.25,"dds":0.4620938628158845,"last_synced_commit":"fbf8a95a155d497dffba6b3a6f4b9c5b4f24150e"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kilianc%2Fmozzarella","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kilianc%2Fmozzarella/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kilianc%2Fmozzarella/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kilianc%2Fmozzarella/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kilianc","download_url":"https://codeload.github.com/kilianc/mozzarella/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":223286385,"owners_count":17120000,"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":["hooks","immer","react","typescript"],"created_at":"2024-11-06T05:09:29.039Z","updated_at":"2024-11-06T05:09:29.714Z","avatar_url":"https://github.com/kilianc.png","language":"TypeScript","funding_links":["https://www.paypal.me/kilianciuffolo"],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n  \u003cbr\u003e\n  \u003cimg src=\"./.github/mozzarella.png\" width=\"500\"\u003e\n  \u003cbr\u003e\n  \u003cbr\u003e\n  \u003cp\u003e\n    A cheezy-simple \u003cb\u003e\u003ccode\u003e679 bytes\u003c/code\u003e\u003c/b\u003e hook based \u003cb\u003e\u003ccode\u003eimmutable store\u003c/code\u003e\u003c/b\u003e, that leverages \u003cb\u003e\u003ccode\u003euseState\u003c/code\u003e\u003c/b\u003e and \u003cb\u003e\u003ccode\u003eImmer\u003c/code\u003e\u003c/b\u003e to create independent rendering trees so that your components \u003cb\u003eonly re-render when they should\u003c/b\u003e.\n  \u003c/p\u003e\n  \u003cdiv align=\"center\"\u003e\n\n![version](https://img.shields.io/npm/v/mozzarella?style=flat-square)\n![size](https://img.shields.io/bundlephobia/minzip/mozzarella?style=flat-square)\n![downloads](https://img.shields.io/npm/dm/mozzarella?style=flat-square)\n\n  \u003c/div\u003e\n  \u003cbr\u003e\n\u003c/div\u003e\n\n\n\n## Motivation\n\nI have been struggling to find a **state management** solution for `react` that makes you interact with your state using plain functions as a baseline. Most of the alternatives I found compromise simplicity, they're verbose or super abstract. I wanted an option that didn't force me to adopt a specific data pattern and was lean.\n\nI don't like boilerplate code. It's the main reason why I stopped using `redux`, but I never stopped chasing most of its design goals. I love how in `redux`, components can be **built in isolation**, **tested easily**, and its overall **separation of concerns**.\n\nWhile using some of the available `redux` alternatives, I kept asking myself:\n\n* *\"Where is the `connect` function?\"*.\n* *\"How do I attach the state to my component without rewriting it?\"*.\n\nThis led to many awkward implementations attempts, that ultimately fell short one way or another.\n\nI also love **TypeScript**, and it has been hard to find a well balanced solution that satisfied all my requirements as well as having strong type support.\n\nLast but not least: *your state management should be easy to understand for someone that didn't participate in the project design choices*.\n\n### Design Goals\n\n* [x] Be as simple as a **mozzarella** (duh!)\n* [x] Use immutability without it getting in the way\n* [x] Use plain JS functions as actions\n* [x] Use async or sync functions for actions\n* [x] Keep actions separated from the store\n* [x] Prevent unnecessary re-rendering of components\n* [x] Batch changes together to prevent race conditions\n* [ ] Batch changes across multiple stores\n* [x] Lean and robust `TypeScript` support\n* [ ] Add dependencies checks (`react-hooks/exhaustive-deps`) for `useDerivedState` hook\n* [ ] Implement concurrency controls similar to [ember-concurrency](http://ember-concurrency.com/docs/task-concurrency)\n\n## Install\n\n    $ yarn add --dev --exact mozzarella immer react-fast-compare\n\n## Basic Example ([try it](https://codesandbox.io/s/mozzarella-basic-8og5b?file=/src/index.tsx))\n\n```tsx\nimport React from 'react'\nimport ReactDOM from 'react-dom'\nimport { createStore } from 'mozzarella'\n\n// create a store and pass an initial state\n\nconst { getState, createAction, useDerivedState } = createStore({\n  names: ['kilian', 'arianna', 'antonia', 'pasquale'],\n  places: ['san francisco', 'gavardo', 'salò']\n})\n\n// a Immer Draft\u003cS\u003e is passed to the action creator\n\nconst addName = createAction((state, name: string) =\u003e {\n  state.names.push(name)\n})\n\nconst addPlace = createAction((state, name: string) =\u003e {\n  state.places.push(name)\n})\n\n// this component only re-renders when `state.names` changes\n\nconst Names = () =\u003e {\n  console.info('\u003cNames /\u003e re-render')\n  const names = useDerivedState(state =\u003e state.names)\n\n  return (\n    \u003cdiv\u003e\n      \u003cbutton onClick={() =\u003e addName('prison mike')}\u003eAdd Prison Mike\u003c/button\u003e\n      \u003cbutton onClick={() =\u003e addPlace('scranton')}\u003eAdd Scranton\u003c/button\u003e\n      \u003ch2\u003eNames:\u003c/h2\u003e\n      \u003cul\u003e\n        {names.map((name, key) =\u003e (\n          \u003cli key={key}\u003e{name}\u003c/li\u003e\n        ))}\n      \u003c/ul\u003e\n      \u003ch2\u003eState:\u003c/h2\u003e\n      \u003cpre\u003e{JSON.stringify(getState(), null, 2)}\u003c/pre\u003e\n    \u003c/div\u003e\n  )\n}\n\nReactDOM.render(\u003cNames /\u003e, document.getElementById('root'))\n```\n\n## Example with pure functional components ([try it](https://codesandbox.io/s/mozzarella-fc-kwcvh?file=/src/index.tsx))\n\n```tsx\n// store.ts\n\nimport { createStore } from 'mozzarella'\n\nexport const { getState, createAction, useDerivedState } = createStore({\n  fruits: []\n})\n```\n\n```ts\n// actions.ts\n\nimport { createAction } from './store'\n\nexport const addFruit = createAction((state, name: string) =\u003e {\n  state.fruits.push(name)\n})\n\nexport const popFruit = createAction((state) =\u003e {\n  state.fruits.pop()\n})\n```\n\n```tsx\n// fruits.tsx\n\nimport React, { FC } from 'react'\nimport * as actions from './actions'\nimport { useDerivedState } from './store'\n\ntype FruitsProps = {\n  fruits: string[]\n  onRemove: () =\u003e void\n  onAdd: (name: string) =\u003e void\n}\n\n// use this in your component stories and docs\nexport const Fruits = ({ fruits, onRemove, onAdd }: FruitsProps) =\u003e (\n  \u003cdiv\u003e\n    \u003ch2\u003eFruits:\u003c/h2\u003e\n    \u003cul\u003e\n      {fruits.map((fruit, key) =\u003e (\n        \u003cli key={key}\u003e{fruit}\u003c/li\u003e\n      ))}\n    \u003c/ul\u003e\n    \u003cbutton onClick={onRemove}\u003eremove last fruit\u003c/button\u003e\n    \u003cbutton onClick={() =\u003e onAdd('bananas')}\u003eadd bananas\u003c/button\u003e\n  \u003c/div\u003e\n)\n\n// use this in your app rendering tree\nFruits.Connected = (() =\u003e {\n  const derivedProps = useDerivedState((state) =\u003e ({\n    fruits: state.fruits,\n    onAdd: actions.addFruit,\n    onRemove: actions.popFruit\n  }))\n\n  return \u003cFruits {...derivedProps} /\u003e\n}) as FC\n```\n\n```tsx\n// index.tsx\n\nimport React from 'react'\nimport ReactDOM from 'react-dom'\nimport { Fruit } from './fruit'\n\nexport const App = () =\u003e (\n  \u003cFruit.Connected /\u003e\n)\n\nReactDOM.render(\u003cApp /\u003e, document.getElementById('app'))\n```\n\n## API Reference\n\n### `createStore`\n\n```ts\ncreateStore \u003cS\u003e(initialState: S) =\u003e {\n  getState: () =\u003e S\n  useDerivedState: \u003cR\u003e(selector: (state: S) =\u003e R) =\u003e R\n  createAction: \u003cU extends unknown[]\u003e(actionFn: (state: Draft\u003cS\u003e, ...params: U) =\u003e void) =\u003e (...params: U) =\u003e void\n}\n```\n\nTakes the initial state as parameter and returns an object with three properties:\n\n* [`getState`](#getState)\n* [`createAction`](#createAction)\n* [`useDerivedState`](#useDerivedState)\n\n**Example**\n\n```ts\ntype State = {\n  users: Record\u003cUser\u003e,\n  photos: Record\u003cPhoto\u003e,\n  albums: Record\u003cAlbum\u003e,\n  likes?: Record\u003cLikes\u003e\n}\n\nconst { getState, createAction, useDerivedState } = createStore\u003cState\u003e({\n  users: {},\n  photos: {},\n  albums: {}\n})\n```\n\n---\n\n### `getState`\n\n```ts\nconst getState = () =\u003e S\n```\n\nReturns the instance of your immutable state\n\n**Example**\n\n```ts\nconst { likes } = getState()\n```\n\n---\n\n### `createAction`\n\n```ts\nconst createAction = \u003cU extends unknown[]\u003e(actionFn: (state: Draft\u003cS\u003e, ...params: U) =\u003e void): (...params: U) =\u003e void\n```\n\nTakes a function as input and returns a *closured* **action** function that can manipulate a `Draft\u003cS\u003e` of your state.\n\n**Examples**\n\n\nAPI call\n\n```tsx\nconst login = createAction(async (state, email: string, password: string) =\u003e {\n  const {\n    err,\n    userId,\n    apiToken\n  } = await apiRequest('/auth', { email, password })\n\n  state.auth = {\n    err,\n    userId,\n    apiToken\n  }\n})\n\n// ...\n\u003cdiv\u003e\n  {auth.err ? \u003ch1\u003eError: {err.message}\u003c/h1\u003e : null}\n  \u003cbutton onClick={() =\u003e login('me@me.com', 'password')}\u003e\n    login\n  \u003c/button\u003e\n\u003c/div\u003e\n// ...\n```\n\nNested actions\n\n```tsx\nconst fetchUsers = createAction(async (state, amount: number) =\u003e {\n  const data = await apiRequest('https://url/data')\n\n  data.users.forEach((user) =\u003e {\n    state.users[user.id] = user\n  })\n\n  setPhotos(data.photos)\n})\n\n// actions that don't use a draft state directly, can be regular functions\nconst setPhotos = (photos: Photo[]) =\u003e {\n  photos.forEach(setPhoto)\n}\n\n// actions that mutate the state draft, use `createAction`\nconst setPhoto = createAction((state, photo: Photo) =\u003e {\n  // all mutations in the same tick, use the same draft\n  // they only trigger a re-render once per tick\n  state.photos[photo.id] = photo\n})\n```\n\nBatching state changes\n\n```tsx\nconst changeName = createAction((state, name: string) =\u003e {\n  state.name = name\n})\n\nfor (let i = 0; i \u003c 100; i++) {\n  // each iteration reuses the same state draft\n  changeName(`name_${i}`)\n}\n\n// components subscribed to `state.name` will only re-render once.\n// `state.name` will only be set once to \"name_99\"\n```\n\n---\n\n### `useDerivedState`\n\n```ts\nconst useDerivedState: \u003cR\u003e(selector: (state: S) =\u003e R, dependencies?: DependencyList) =\u003e R\n```\n\nHook that given a **selector function**, will return the output of the selector and re-render the component only when it changes.\n\n[As per usual](https://reactjs.org/docs/hooks-reference.html#usememo), this hook takes an optional `dependencies` parameter `that defaults to `[]`.\n\n**Example**\n\n```tsx\nconst UserProfile = (props: { user: User, photos: Photo[] }) =\u003e {\n  return (\n    \u003cdiv\u003e\n      \u003ch1\u003eUser Profile: {props.user.username} ({props.photos.length} photos)\u003c/h1\u003e\n      \u003cdiv\u003e\n        {props.photos.map((photo) =\u003e \u003cPhoto key={photo.id} photo={photo} /\u003e)}\n      \u003c/div\u003e\n    \u003c/div\u003e\n  )\n}\n\nUserProfile.connected = (props: { userId: string }) =\u003e {\n  const derivedProps = useDerivedState((state) =\u003e {\n    user: state.users[props.userId],\n    photos: Object.values(state.photos).filter((photo) =\u003e photo.userId === props.userId)\n  }, [props.userId])\n\n  return \u003cUserProfile {...derivedProps} /\u003e\n}\n```\n\nOr if you're not being dogmatic about it, or simply not implementing a strict design system:\n\n```tsx\nconst UserProfile = (props: { userId: string }) =\u003e {\n  const { user, photos } = useDerivedState((state) =\u003e {\n    user: state.users[props.userId],\n    photos: Object.values(state.photos).filter((photo) =\u003e photo.userId === props.userId)\n  }, [props.userId])\n\n  return (\n    \u003cdiv\u003e\n      \u003ch1\u003eUser Profile: {user.username} ({photos.length} photos)\u003c/h1\u003e\n      \u003cdiv\u003e\n        {photos.map((photo) =\u003e \u003cPhoto key={photo.id} photo={photo} /\u003e)}\n      \u003c/div\u003e\n    \u003c/div\u003e\n  )\n}\n```\n\n## How to contribute\n\nContributions and bug fixes from the community are welcome. You can run the test suite locally with:\n\n    $ yarn lint\n    $ yarn test\n\n## License\n\nThis software is released under the MIT license cited below.\n\n    Copyright (c) 2020 Kilian Ciuffolo, me@nailik.org. All Rights Reserved.\n\n    Permission is hereby granted, free of charge, to any person\n    obtaining a copy of this software and associated documentation\n    files (the 'Software'), to deal in the Software without\n    restriction, including without limitation the rights to use,\n    copy, modify, merge, publish, distribute, sublicense, and/or sell\n    copies of the Software, and to permit persons to whom the\n    Software is furnished to do so, subject to the following\n    conditions:\n\n    The above copyright notice and this permission notice shall be\n    included in all copies or substantial portions of the Software.\n\n    THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,\n    EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n    OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n    NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n    HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n    WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n    OTHER DEALINGS IN THE SOFTWARE.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkilianc%2Fmozzarella","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkilianc%2Fmozzarella","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkilianc%2Fmozzarella/lists"}