{"id":26022809,"url":"https://github.com/rinaldo/redux-optix","last_synced_at":"2025-06-26T12:31:54.024Z","repository":{"id":44019336,"uuid":"231493492","full_name":"Rinaldo/redux-optix","owner":"Rinaldo","description":"Generate a set of Redux action creators and a reducer with a simple, lens-inspired syntax","archived":false,"fork":false,"pushed_at":"2023-01-05T04:50:45.000Z","size":1322,"stargazers_count":1,"open_issues_count":14,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-06-22T12:46:51.351Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","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/Rinaldo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-01-03T02:06:27.000Z","updated_at":"2020-02-13T19:30:05.000Z","dependencies_parsed_at":"2023-02-03T10:16:21.344Z","dependency_job_id":null,"html_url":"https://github.com/Rinaldo/redux-optix","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/Rinaldo/redux-optix","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Rinaldo%2Fredux-optix","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Rinaldo%2Fredux-optix/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Rinaldo%2Fredux-optix/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Rinaldo%2Fredux-optix/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Rinaldo","download_url":"https://codeload.github.com/Rinaldo/redux-optix/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Rinaldo%2Fredux-optix/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":262068035,"owners_count":23253717,"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":[],"created_at":"2025-03-06T10:37:00.383Z","updated_at":"2025-06-26T12:31:53.999Z","avatar_url":"https://github.com/Rinaldo.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Redux Optix\n\n`npm install redux-optix`\n\n**Generate a set of Redux action creators and a reducer with a simple, lens-inspired syntax**\n\nRedux Optix generates a set of action creators and a reducer from a set of declarative definitions. It sets intelligent defaults and works well with libraries like [Ramda](https://ramdajs.com/) to reduce boilerplate and make even complex actions simple to define. Redux Optix centralizes action logic instead of spreading it across an action creator and one or more reducers. This makes the full effects of actions more clear and sidesteps issues with [sharing data between slice reducers](https://redux.js.org/recipes/structuring-reducers/beyond-combinereducers).\n\n## Refactoring with Redux Optix: Fewer lines, more clarity\n\n\u003e The following examples are adapted from the [Redux TodoMVC Example](https://github.com/reduxjs/redux/tree/master/examples/todomvc). They are functionally equivalent\n\n#### Redux Optix\n\n```javascript\nimport * as R from \"ramda\"\nimport { createStore } from \"redux\"\nimport { createOptix } from \"redux-optix\"\nimport { VisibilityFilters } from \"./somewhere\"\n\nconst initialState = {\n  todos: [],\n  visibilityFilter: VisibilityFilters.SHOW_ALL,\n}\n\nconst actionMap = {\n  addTodo: {\n    path: \"todos\",\n    handler: text =\u003e\n      R.append([\n        {\n          text,\n          id: state.reduce((maxId, todo) =\u003e Math.max(todo.id, maxId), -1) + 1,\n          completed: false,\n        },\n      ]),\n  },\n  deleteTodo: {\n    path: \"todos\",\n    handler: id =\u003e R.filter(todo =\u003e todo.id !== id),\n  },\n  editTodo: {\n    path: \"todos\",\n    payloadCreator: (id, text) =\u003e ({ id, text }),\n    handler: ({ id, text }) =\u003e R.map(todo =\u003e (todo.id === id ? { ...todo, text } : todo)),\n  },\n  toggleTodo: {\n    path: \"todos\",\n    handler: id =\u003e R.map(todo =\u003e (todo.id === id ? { ...todo, completed: !todo.completed } : todo)),\n  },\n  setVisibilityFilter: {\n    path: \"visibilityFilter\",\n  },\n}\n\nconst { reducer, actions } = createOptix(actionMap, { initialState })\n\nconst store = createStore(reducer)\n```\n\n#### Vanilla Redux\n\n```javascript\nimport { combineReducers, createStore } from \"redux\"\nimport { VisibilityFilters } from \"./somewhere\"\n\nconst actionTypes = {\n  addTodo: \"addTodo\",\n  deleteTodo: \"deleteTodo\",\n  editTodo: \"editTodo\",\n  toggleTodo: \"toggleTodo\",\n  setVisibilityFilter: \"setVisibilityFilter\",\n}\n\nconst actions = {\n  addTodo: text =\u003e ({\n    type: actionTypes.addTodo,\n    payload: text,\n  }),\n  deleteTodo: id =\u003e ({\n    type: actionTypes.toggleTodo,\n    payload: id,\n  }),\n  editTodo: (id, text) =\u003e ({\n    type: types.editTodo,\n    payload: { id, text },\n  }),\n  toggleTodo: id =\u003e ({\n    type: actionTypes.toggleTodo,\n    payload: id,\n  }),\n  setVisibilityFilter: filter =\u003e ({\n    type: actionTypes.setVisibilityFilter,\n    payload: filter,\n  }),\n}\n\nconst todosReducer = (state = [], action) =\u003e {\n  switch (action.type) {\n    case actionTypes.addTodo:\n      return state.concat([\n        {\n          text: action.payload,\n          id: state.reduce((maxId, todo) =\u003e Math.max(todo.id, maxId), -1) + 1,\n          completed: false,\n        },\n      ])\n    case actionTypes.deleteTodo:\n      return state.filter(todo =\u003e todo.id !== action.payload)\n    case actionTypes.editTodo:\n      return state.map(todo =\u003e\n        todo.id === action.payload.id ? { ...todo, text: action.payload.text } : todo\n      )\n    case actionTypes.toggleTodo:\n      return state.map(todo =\u003e\n        todo.id === action.payload ? { ...todo, completed: !todo.completed } : todo\n      )\n    default:\n      return state\n  }\n}\n\nconst visibilityFilterReducer = (state = VisibilityFilters.SHOW_ALL, action) =\u003e {\n  switch (action.type) {\n    case actionTypes.setVisibilityFilter:\n      return action.payload\n    default:\n      return state\n  }\n}\n\nconst rootReducer = combineReducers({\n  todos,\n  visibilityFilter,\n})\n\nconst store = createStore(rootReducer)\n```\n\n## Actions in detail\n\n#### A Basic Action\n\n```javascript\nsetUserName: {\n  path: \"user.name\"\n}\n```\n\nSpecifying just a [path](#path) will create a setter function due to the defaults on other properties.\n\n#### Defining a handler\n\n```javascript\ndeleteTodo: {\n  path: \"todos\",\n  handler: id =\u003e R.filter(todo =\u003e todo.id !== id)\n}\n```\n\nThe [handler](#handler) property serves as the case reducer for the action. It is a higher-order function that is called with the the action's payload (and meta if applicable), then with a slice of state, and should return a new value for that slice. Redux Optix works well with the curried, data-last functions of libraries like [Ramda](https://ramdajs.com/). The vanilla js version of this example would be `id =\u003e todos =\u003e todos.filter(todo =\u003e todo.id !== id)`\n\n#### Setting the arity of the default action creator\n\n```javascript\neditTodo: {\n  arity: 2,\n  path: \"todos\",\n  handler: (text, id) =\u003e R.map(todo =\u003e todo.id === id ? { ...todo, text } : todo)\n}\n```\n\nThe default action creator takes one argument and sets it as the action's payload. Specifying an [arity](#arity) of 2 will generate an action creator that takes two arguments and sets them as the action's payload and meta properties.\n\n#### Defining a customized action creator\n\n```javascript\naddTodo: {\n  path: \"todos\",\n  payloadCreator: (...words) =\u003e words.join(\" \"),\n  metaCreator: (...words) =\u003e Math.floor(Math.random() * words.length),\n  handler: (text, id) =\u003e R.append({ text, id, completed: false })\n}\n```\n\nMore complex action creators taking any number of arguments can be defined by using the [payloadCreator](#payloadCreator) and [metaCreator](#metaCreator) properties. Since both [payloadCreator](#payloadCreator) and [metaCreator](#metaCreator) are specified in this example, [handler](#handler) will be called with 2 arguments.\n\n#### Setting state to a constant\n\n```javascript\nfetchRequest: {\n  path: \"request.status\",\n  always: \"LOADING\"\n}\n```\n\nShorthand for [always](#always) setting a value into state.\n\n#### Updating multiple pieces with a shorter path\n\n```javascript\nfetchSuccess1: {\n  path: \"request\",\n  handler: data =\u003e R.mergeLeft({ data, status: \"DONE\" })\n}\n```\n\nA shorter path gives the handler access to more of the state. The above handler updates both `request.data` and `request.status`.\n\n#### Updating multiple pieces of state with batch\n\n```javascript\nfetchSuccess2: {\n  batch: [\n    { path: \"request.status\", always: \"DONE\" },\n    { path: \"request.data\" },\n    { path: \"loadingState\", handler: () =\u003e R.dec },\n  ]\n}\n```\n\nA [batch](#batch) reducer operation can be used to update multiple disparate pieces of state.\n\n#### Sharing properties between batches\n\n```javascript\nfetchSuccess3: {\n  path: \"request\",\n  batch: [\n    { suffix: \"status\", always: \"DONE\" },\n    { suffix: \"data\" },\n    { path: \"loadingState\", handler: () =\u003e R.dec },\n  ]\n}\n```\n\nAny properties specified at the top level will be merged with the batch properties. In this case the [path](#path) `request` will be shared between the first two items and the value of the [suffix](#suffix) property will be appended.\n\n#### validating actions\n\n```javascript\nincrementUpToTen: {\n  path: \"counter\",\n  arity: 0,\n  handler: R.inc,\n  validate: ({ slice }) =\u003e slice \u003c 10\n}\n```\n\nActions can be [validated](#validate) with a predicate function and will only be dispatched if the predicate function returns true. Async predicates are also supported.\n\n## API Reference\n\n### `createOptix`\n\nThe one export of Redux Optix. It takes an `actionMap` argument and an optional `options` argument. It returns an object with the following properties:\n\n- `actions`: An object with the same keys as `actionMap`. The value of each key is an action creator with a `toString` method that returns the action type.\n- `types`: An object with the same keys as `actionMap`. The value of each key is the action type.\n- `reducer`: Reducer function that handles all actions specified in `actionMap`.\n\n### the `actionMap` argument\n\nEach key will name an action creator and each value is an action/reducer definition that may contain the following properties:\n\n#### `handler`\n\n`() =\u003e stateSlice =\u003e newStateSlice`  \n`payload =\u003e stateSlice =\u003e newStateSlice`  \n`(payload, meta) =\u003e stateSlice =\u003e newStateSlice`\n\nUpdates the piece of state at the path specified. It is first called with the contents of the action (see `arity`), then with the piece of state, and should return an update to that piece of state. The default value is `payload =\u003e () =\u003e payload`.\n\n#### `always`\n\n`any`\n\nShorthand for setting state to a constant value. `always` is ignored if set to `undefined` or if `handler` is specified.\n\n#### `arity`\n\n`0 | 1 | 2`\n\nDetermines how many arguments the `handler` function is initially called with. Also sets the arity of the default action creator if neither `payloadCreator` nor `metaCreator` are specified. If `arity` is 1, the handler is called with the action's payload. If it is 2, the handler is called with both the action's payload and meta properties. The default value is 1 except it's 2 when `metaCreator` is specified and it's 0 when `always` is specified and `handler` is not.\n\n#### `payloadCreator`\n\n`(...args) =\u003e payload`\n\nTakes any number of arguments and returns a value for the action payload. If `arity` is 1 or 2 the default value is a function that returns its first argument.\n\n#### `metaCreator`\n\n`(...args) =\u003e meta`\n\nSimilar to `payloadCreator`. Takes any number of arguments (the same arguments as the `payloadCreator`) and returns a value for the action's meta property. If `arity` is 2 the default value is a function that returns its second argument.\n\n#### `path`\n\n`string | Array\u003cstring\u003e`\n\nSpecifies a path into the state object. The value at that path will be passed to the `handler` function. `path` can be an array of keys or a string containing one or more keys, i.e. `\"user.todos[0].text\"`. If `path` is empty or undefined, the entire state will be passed to the `handler` function.\n\n#### `suffix`\n\n`string | Array\u003cstring\u003e`\n\nSpecifies an additional path that will be appended to the value of `path`.\n\n#### `batch`\n\n`Array\u003cProperties\u003e`\n\nUpdates multiple pieces of state. Any properties specified outside `batch` will be merged with (but will not overwrite) the `batch` properties. `batch` supports `handler`, `always`, `arity`, `path`, and `suffix`.\n\n#### `validate`\n\n`(params: ValidateParams) =\u003e boolean | Promise\u003cboolean\u003e`\n\n\u003e [Redux Thunk](https://github.com/reduxjs/redux-thunk) is required to use the `validate` property\n\nPredicate function that prevents invalid actions from being dispatched. It replaces the plain action creator normally returned by `createOptix` with a thunk that dispatches the underlying action if validation succeeds. The thunk returns the dispatched action if validation succeeds or false if it fails. The validated action is dispatched synchronously unless an async predicate is used. In the case of an async predicate the thunk will return a promise for either the dispatched action or false.\n\n- `ValidateParams`\n\n  - `state`: the full state object\n  - `slice`: the piece of state found at the resolved `path`\n  - `payload`: the action's payload property\n  - `meta`: the action's meta property\n  - `extra`: Redux Thunk's [extra argument](#https://github.com/reduxjs/redux-thunk#injecting-a-custom-argument)\n\n  \u003e Note: the `state` and `slice` params are getters so state can be accessed asynchronously.\n\n### the `options` argument\n\nThe options object may contain the following properties:\n\n#### `initialState`\n\n`any`\n\nDefines the initial state of the reducer.\n\n#### `formatActionTypes`\n\n`(actionCreatorName: string) =\u003e string`\n\nCustomizes generated action types if a format like CONSTANT_CASE is desired.\n\n## Recipes\n\n#### Reusable Action Groups\n\nThe following function can be used to generate a reusable set of fetch actions\n\n```javascript\nconst createFetchActions = (namePrefix, path) =\u003e {\n  const requestType = namePrefix + \"FetchRequest\"\n  const successType = namePrefix + \"FetchSuccess\"\n  const errorType = namePrefix + \"FetchError\"\n  return {\n    [requestType]: {\n      path,\n      suffix: \"status\",\n      always: \"LOADING\",\n    },\n    [successType]: {\n      path,\n      batch: [{ suffix: \"status\", always: \"DONE\" }, { suffix: \"data\" }],\n    },\n    [errorType]: {\n      path,\n      batch: [{ suffix: \"status\", always: \"ERROR\" }, { suffix: \"error\" }],\n    },\n  }\n}\n\nconst actionMap = {\n  // some other actions\n  ...createFetchActions(\"entitlements\", \"user.entitlements\"),\n  ...createFetchActions(\"savedPosts\", \"user.savedPosts\"),\n}\n```\n\n#### State Machine\n\nValidations can be used to make a state machine\n\n```javascript\nconst mediaMachine = {\n  play: {\n    always: \"PLAYING\",\n    validate: ({ state }) =\u003e state === \"PAUSED\" || state === \"STOPPED\",\n  },\n  pause: {\n    always: \"PAUSED\",\n    validate: ({ state }) =\u003e state === \"PLAYING\",\n  },\n  stop: {\n    always: \"STOPPED\",\n    validate: ({ state }) =\u003e state === \"PLAYING\" || state === \"PAUSED\",\n  },\n}\n\nconst { reducer, actions, types } = createOptix(mediaMachine, { initialState: \"STOPPED\" })\n```\n\n## FAQ\n\n#### Is Redux Optix [FSA](https://github.com/redux-utilities/flux-standard-action) Compliant?\n\nYes!\n\n#### Why use Redux Optix over other Redux helper libraries?\n\n- Action-Centric: All logic for a given action is centralized, avoiding issues with [sharing data between slice reducers](https://redux.js.org/recipes/structuring-reducers/beyond-combinereducers)\n- Less Boilerplate: A path is all that is needed to define a a simple setter action\n- Optimized for Ramda: Functions like `R.append`, `R.filter`, and `R.inc` can be used directly as handlers\n\n#### What if I like `combineReducers` or I only want to use Redux Optix in one part of my Redux state?\n\nThat's ok! Redux Optix just generates some actions and a reducer. The generated reducer can be used with `combineReducers` or any other Redux helper function.\n\n#### How does Redux Optix scale as an application grows?\n\nRedux Optix scales very well. If the `actionMap` object gets too big for one file, different pieces of it can be written in different files and combined using the spread operator before being passed to `createOptix`.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frinaldo%2Fredux-optix","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frinaldo%2Fredux-optix","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frinaldo%2Fredux-optix/lists"}