{"id":20801141,"url":"https://github.com/pothos-dev/react-zeno","last_synced_at":"2025-05-07T00:10:36.873Z","repository":{"id":42914483,"uuid":"248869560","full_name":"pothos-dev/react-zeno","owner":"pothos-dev","description":"The React companion to Zeno, a Redux implementation optimized for Typescript.","archived":false,"fork":false,"pushed_at":"2023-01-05T19:29:24.000Z","size":2627,"stargazers_count":13,"open_issues_count":14,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-03-31T04:22:49.858Z","etag":null,"topics":["hooks","react","redux","state-management","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/pothos-dev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-03-20T23:11:58.000Z","updated_at":"2024-09-21T13:45:43.000Z","dependencies_parsed_at":"2023-02-04T11:46:48.528Z","dependency_job_id":null,"html_url":"https://github.com/pothos-dev/react-zeno","commit_stats":null,"previous_names":["bearbytes/react-zeno"],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pothos-dev%2Freact-zeno","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pothos-dev%2Freact-zeno/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pothos-dev%2Freact-zeno/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pothos-dev%2Freact-zeno/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pothos-dev","download_url":"https://codeload.github.com/pothos-dev/react-zeno/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252788529,"owners_count":21804284,"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","react","redux","state-management","typescript"],"created_at":"2024-11-17T18:16:51.708Z","updated_at":"2025-05-07T00:10:36.853Z","avatar_url":"https://github.com/pothos-dev.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![NPM Version](https://img.shields.io/npm/v/@bearbytes/react-zeno.svg?style=flat)](https://www.npmjs.com/package/@bearbytes/react-zeno)\n[![Actions Status](https://github.com/bearbytes/react-zeno/workflows/CI/badge.svg)](https://github.com/bearbytes/react-zeno/actions)\n\n# React-Zeno\n\n\u003c!-- TOC depthFrom:2 --\u003e\n\n- [What is Zeno?](#what-is-zeno)\n- [Getting Started](#getting-started)\n  - [Installation](#installation)\n  - [Defining Types](#defining-types)\n  - [Creating a Store](#creating-a-store)\n  - [Dispatching messages](#dispatching-messages)\n  - [Reading Store state](#reading-store-state)\n- [Advanced Topics](#advanced-topics)\n  - [Side effects and async functions](#side-effects-and-async-functions)\n  - [Subscribe to changes](#subscribe-to-changes)\n  - [Middleware](#middleware)\n  - [Redux DevTools Integration](#redux-devtools-integration)\n  - [Creating additional store instances](#creating-additional-store-instances)\n- [FAQ](#faq)\n  - [Is this compatible with Redux middleware?](#is-this-compatible-with-redux-middleware)\n  - [Why \"message\" over \"action\"?](#why-message-over-action)\n  - [Why \"messageHandler\" over \"reducer\"?](#why-messagehandler-over-reducer)\n- [Future Work](#future-work)\n  - [Sending Messages from Redux DevTools Extensions](#sending-messages-from-redux-devtools-extensions)\n  - [Slices](#slices)\n  - [Internal Messages](#internal-messages)\n\n\u003c!-- /TOC --\u003e\n\n## What is Zeno?\n\nZeno is a variant of the [Redux](https://github.com/reduxjs/redux) pattern, but is written from the ground up to make the best use of Typescripts powerful type inference capabilities.\n\nThis library is the same as [zeno](https://github.com/bearbytes/zeno), but also provides typesafe React Hooks.\n\nIt aims to:\n\n- provide auto-completion\n- minimize the amount of boilerplate\n- without sacrificing type safety\n\nIt is also opinionated in these ways:\n\n- use [Immer](https://github.com/immerjs/immer) to allow for direct state mutation\n- use different terms as the original Redux (see [why](#why-message-over-action))\n\nIf you're coming from Redux, here is a glossary with the terms you are familiar with:\n\n| Redux    | Zeno                                      |     |\n| -------- | ----------------------------------------- | --- |\n| Store    | StoreInterface, StoreClass, StoreInstance | ❗  |\n| Action   | Message                                   | ❗  |\n| Dispatch | Dispatch                                  | ✔️  |\n| State    | State                                     | ✔️  |\n| Reducer  | MessageHandler                            | ❗  |\n\n## Getting Started\n\n### Installation\n\n```\nnpm i @bearytes/react-zeno\n# or\nyarn add @bearbytes/react-zeno\n```\n\n### Defining Types\n\nHere is an example for the `StoreInterface` of a Todo App:\n\n```ts\n// This is called the interface of a store: It defines the public surface (state and messages).\ntype TodoStore = {\n  state: {\n    // List of Todos\n    todos: TodoItem[]\n\n    // The id to be assigned to the next TodoItem\n    nextId: number\n  }\n\n  messages: {\n    // Create a new TodoItem.\n    createTodo: { text: string }\n\n    // Change the name of an existing TodoItem.\n    changeText: { id: number; newText: string }\n\n    // Mark an existing item as done.\n    markAsDone: { id: number }\n  }\n}\n\ntype TodoItem = { id: number; text: string; done?: boolean }\n```\n\nAs you see, you define the types for the `state`, as well as the names and payloads of `messages` all in one place, without the need for any helper functions or generic types.\n\nThis is the only place where you need to mention these types, they will be inferred automatically everywhere else.\n\n### Creating a Store\n\nWe have to differentiate between a `StoreClass` and a `StoreInstance`.\n\n`StoreClass`:\n\n- define `state` and `messages` types (as shown above)\n- defines behavior using `messageHandlers`\n- include at least 1 `StoreInstance`, but can spawn additional instances\n\n`StoreInstance`:\n\n- contains the actual `state` values\n- receives `messages` and executes the `messageHandlers`\n\nIn most cases, you will only ever use the singular `StoreInstance`, but there might be cases where different parts of your application want to manage their own copy of the state.\n\nTo create both the `StoreClass`, use the `createStoreClass` method:\n\n```ts\nconst storeClass = createStoreClass\u003cTodoStore\u003e({\n  initialState: {\n    // Start with an empty list of Todos.\n    todos: [],\n  },\n\n  messageHandlers: {\n    // Create a new TodoItem.\n    createTodo(s, m) {\n      const todo = { id: s.nextId, text: m.text }\n      s.todos.push(todo)\n      s.nextId++\n    },\n\n    // Change the name of an existing TodoItem.\n    changeText(s, m) {\n      const todo = s.todos.find((todo) =\u003e todo.id == m.id)!\n      todo.text = m.newText\n    },\n\n    // Mark an existing item as done.\n    markAsDone(s, m) {\n      const todo = s.todos.find((todo) =\u003e todo.id == m.id)!\n      todo.done = true\n    },\n  },\n})\n```\n\nThe shape of this code mirrors the Type definition we created above.\n\nBy passing the `StoreInterface` as generic argument to `createStoreClass`, the compiler will autocomplete the names of the messages and provide correct type information for the state type (`s`) and the message payloads (`m`).\n\nThe `StoreClass` can be destructured into these values:\n\n```ts\nconst {\n  // Primary StoreInstance, used by default when using Hooks\n  defaultInstance,\n  // Hook to access some state from the Store\n  useStore,\n  // Hook to access the dispatch method of the Store\n  useDispatch,\n  // Hook to get the Store instance that is used in this component subtree\n  useStoreInstance,\n  // Context Provider component to use a different StoreInstance in the component subtree\n  StoreContainer,\n} = storeClass\n```\n\n### Dispatching messages\n\nA `message` must be dispatched to a specific `StoreInstance`:\n\n```ts\n// with hooks\nconst dispatch = useDispatch()\ndispatch({ type: 'markAsDone', id: 42 })\n\n// or anywhere with a storeInstance\nstoreInstance.dispatch({ type: 'markAsDone', id: 42 })\n```\n\nThe `dispatch` function is fully typed and will autocomplete message types and their corresponding payloads.\n\n### Reading Store state\n\nThe `state` must be read from a specific `StoreInstance`:\n\n```ts\n// with hooks - subscribes to changes and rerenders the component automatically\nconst currentState = useStore()\n\n// will only re-render the component if the selected state changes\nconst selectedState = useStore((s) =\u003e s.todos)\n\n// or anywhere with a storeInstance\nconst currentState = storeInstance.getState()\n```\n\n## Advanced Topics\n\n### Side effects and async functions\n\nThe Redux pattern is synchronous - at each point in time, the `state` must be valid. This is a problem when interacting with asynchronous operations, like fetching data from the network.\n\nThe usual workaround is to define a `message` that starts an asynchronous operation, and then another message that updates the Store when the operation completes.\n\nZeno implements the [Thunk](https://github.com/reduxjs/redux-thunk) pattern, where you can return a function from `messageHandler` that has access to the `StoreInstance` and can synchronously or asynchronously dispatch new `messages`:\n\nAlternatively, the `StoreInstance` is passed as the third parameter to each `messageHandler` and can be used in the same way.\n\nSee this example:\n\n```ts\ntype Store = {\n  state: {\n    data?: any\n    lastError?: string\n    fetchInProgress: boolean\n  }\n\n  messages: {\n    fetch: {}\n    fetchFinished: { data?: any; error?: string }\n    clearError: {}\n  }\n}\n\nconst storeClass = createStoreClass\u003cStore\u003e({\n  initialState: {\n    fetchInProgress: false,\n  },\n\n  messageHandlers: {\n    fetch(s, m) {\n      // The messageHandler updates the state synchronously...\n      if (s.fetchInProgress) {\n        s.lastError = 'Another fetch is already in progress.'\n      } else {\n        s.fetchInProgress = true\n        // ...and starts of an asynchronous operation,\n        // which will dispatch another message when done.\n        return async (dispatch) =\u003e {\n          const { data, error } = await downloadDataAsync()\n          dispatch({ type: 'fetchFinished', data, error })\n        }\n      }\n    },\n\n    // extract the dispatch function of the executing store instance instead using Thunk\n    fetchFinished(s, m, { dispatch }) {\n      s.fetchInProgress = false\n      s.data = m.data\n      // A messageHandler may also dispatch synchronously.\n      // The dispatched message will be executed immediately afterwards,\n      // before the views have a chance to re-render.\n      dispatch({ type: 'clearError' })\n    },\n\n    clearError(s) {\n      s.lastError = undefined\n    },\n  },\n})\n```\n\n### Subscribe to changes\n\nYou can `subscribe` to a `storeInstance` by passing a callback.\n\nThe callback will be updated when the state is updated.\n\nIf the second parameter of `subscribe` is `true`, the callback will also be executed immediately with the current state.\n\n```ts\nconst unsubscribe = storeInstance.subscribe(\n  (s) =\u003e console.log(s),\n  true /* immediately logs the current state. optional parameter */\n)\nunsubscribe()\n```\n\n### Middleware\n\nZeno implements the Redux Middleware API and is compatible with existing middleware libraries.\n\nA middleware has this form:\n\n```ts\nconst exceptionHandlerMiddleware = (store) =\u003e (next) =\u003e (actionOrMessage) =\u003e {\n  // we have access to the store instance\n  const prevState = store.getState()\n\n  // call the next middleware (or finally the registered messageHandler)\n  try {\n    const nextState = next(actionOrMessage)\n    // return the updated state from middleware\n    return nextState\n  } catch (error) {\n    // or do some other things, like logging and\n    // returning the previous state in case of an error\n    console.error(error.message)\n    return prevState\n  }\n}\n```\n\nYou can pass any number of middlewares to `createStoreClass`:\n\n```ts\nconst storeClass = createStoreClass\u003cMyStore\u003e({\n  initialState: {...}\n  messageHandlers: {...}\n  middleware: [loggerMiddleware, exceptionHandlerMiddleware]\n})\n```\n\n### Redux DevTools Integration\n\n`StoreInstance`s will automatically connect to a Redux DevTools Extension in the Browser, if available.\n\nIt is possible to pass configuration for the Integration to `createStoreClass` and overwrite it in `createInstance`.\n\nIt is currently not possible to send Messages from the DevTools to the Store, but this feature [can be added](#sending-messages-from-redux-devtools-extensions) if requested.\n\n### Creating additional store instances\n\nYou can call `createInstance` on a `StoreClass` to create a new copy of the state.\n\nOptionally, you can pass an initial state into the instance, otherwise it will use the `initialState` from `createStoreClass`.\n\n```ts\nasync function initializeFromStorage(): StoreInstance\u003cany\u003e {\n  const loadedState = await loadFromStorage()\n  return storeClass.createInstance(loadedState)\n}\n```\n\nIf you pass an array of middlewares, they will be called in the given order.\n\n## FAQ\n\n### Is this compatible with Redux middleware?\n\nYes. However, this library cannot guarantee that any outside middleware conforms to the `state` and `message` types defined for a Zeno store. Middlewares could dispatch actions or create state that cannot be dealt with in a type-safe way. This is not a problem for most middlewares though, as these implement features that do not interfere with the predefined types.\n\n### Why \"message\" over \"action\"?\n\nOne reason to use the Redux pattern is to separate the concerns of \"what happens\" with \"how does the state change\". Essentially, we are implementing a [Publish–subscribe pattern](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern), where the UI (or other parts of the applications) publish events, and the Store subscribes to these events to update its internal state.\n\nPassing detailed instructions to the Store about how the state should exactly change, breaks the encapsulation and tightly couples the business logic with the event dispatcher (that often lives in the UI). So if we'd often have actions called `'setFoo'`, we would be missing the point of Redux, [as stated by Dan Abramov](https://twitter.com/dan_abramov/status/800310397538619393).\n\nDue to this reason, I find the term \"_action_\" to be confusing, as it implies a specific instruction, rather than an informative event, that the Store is free to act on in any way appropriate. However, using the term \"_event_\" is also quite restricting, as now the user is encouraged to write `'somethingHappened'` events instead of `'doSomething'` actions.\n\nI chose the term \"_message_\", as this word carries all the right connotations without implying a certain way of thinking, opening it up for event- and action-based usages. It simply implies an asynchronous packet of information with no added behavior, that can be serialized and sent over a network (as is often useful, for example when working with the Redux DevTools).\n\n### Why \"messageHandler\" over \"reducer\"?\n\nThe term \"_reducer_\" is named after [an operation often used in functional programming](\u003chttps://en.wikipedia.org/wiki/Fold_(higher-order_function)\u003e), where an initial value (`state`) and any number of additional values (`action`s) are _reduced_ to a single value again (the next `state`). In fact, this is exactly how Redux works and is an appropriate, albeit sometimes confusing term.\n\nHowever, using Reducers forces the programmer to consider their state immutable, and create new copies of the state, which often leads to a lot of boilerplate and hard to read code. For this reason, nowadays it is often recommended to use [Immer](https://github.com/immerjs/immer) to allow mutating the state in-place. This library does the same thing. But while there is still a `reduce` happening in the internals of the library, the actual user code written to handle a message can no longer be best described by the term \"_reducer_\".\n\nThe term \"_messageHandler_\" simply states that we need to deal with a message in any way. This might be updating state, creating side-effects (like starting network requests) or dispatching additional messages. It doesn't imply that we are performing a `reduce` operation, like the original term would.\n\n## Future Work\n\n### Sending Messages from Redux DevTools Extensions\n\nFor this to work, the lifetime of a StoreInstance must be tracked, otherwise subscribing to DevTools will create memory leaks.\n\nThe [Redux DevTools](https://github.com/reduxjs/redux-devtools) are very useful when debugging what happens in the Store, and should probably be built into most libraries that implement Redux.\n\n### Slices\n\nAs the Store grows and features ever more messages, it is useful to break it up into different parts, which are called [Slice](https://redux-toolkit.js.org/tutorials/basic-tutorial#introducing-createslice) in the Redux world.\n\n### Internal Messages\n\nJust as classes have public and private methods, a Store might have public and private messages. From the outside, only public messages can be dispatched, but the `messageHandlers` of the store might also dispatch private messages, which allows the programmer to extract repeated tasks into an internal `messageHandler`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpothos-dev%2Freact-zeno","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpothos-dev%2Freact-zeno","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpothos-dev%2Freact-zeno/lists"}