{"id":19273959,"url":"https://github.com/rintoj/redux-saga-tools","last_synced_at":"2025-04-21T22:33:21.442Z","repository":{"id":48028662,"uuid":"156360508","full_name":"rintoj/redux-saga-tools","owner":"rintoj","description":"Utility functions to write saga and reducers quickly and easily","archived":false,"fork":false,"pushed_at":"2021-08-10T20:29:18.000Z","size":39,"stargazers_count":4,"open_issues_count":1,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-01T16:24:13.375Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"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/rintoj.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-11-06T09:41:38.000Z","updated_at":"2020-01-23T08:29:36.000Z","dependencies_parsed_at":"2022-08-12T17:01:01.571Z","dependency_job_id":null,"html_url":"https://github.com/rintoj/redux-saga-tools","commit_stats":null,"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rintoj%2Fredux-saga-tools","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rintoj%2Fredux-saga-tools/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rintoj%2Fredux-saga-tools/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rintoj%2Fredux-saga-tools/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rintoj","download_url":"https://codeload.github.com/rintoj/redux-saga-tools/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249984483,"owners_count":21356066,"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":"2024-11-09T20:44:40.774Z","updated_at":"2025-04-21T22:33:16.419Z","avatar_url":"https://github.com/rintoj.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\n# Redux Saga Tools\n\nRedux Saga Tools is a collection of utility functions to accelerate the development of a redux-saga application, by providing abstractions over most common redux tasks and patterns. This library is build on top of [redux-saga](https://redux-saga.js.org/) - a library that aims to make application side effects easier to manage, more efficient to execute, simple to test, and better at handling failures.\n\nThis library is fully compatible with TypeScript.\n\n# Install\n\n```bash\nnpm install redux-saga-tools\n```\n\n# API\n\n## 1. Saga\n\n### `createSaga(action: string, api: Function, successAction?: string, failureAction?: string)`\n\nCreates a saga that will automatically generates `_SUCCESS` and `_ERROR` as the default success and failure actions, if `successAction` and `failureActions` are not provided. This function is best suited for request-response type of interaction with an external API.\n\n```ts\nimport { createSaga } from 'redux-saga-tools'\nimport { UserActionType } from './user-actions'\n\nexport default [\n  createSaga(\n    UserActionType.SIGN_IN_USER,\n    userApi.signIn\n  )\n]\n// will emit SIGN_IN_USER_SUCCESS or SIGN_IN_USER_ERROR\n```\n\nor\n\n```ts\nimport { createSaga } from 'redux-saga-tools'\nimport { UserActionType } from './user-actions'\n\nexport default [\n  createSaga(\n    UserActionType.SIGN_IN_USER,\n    userApi.signIn,\n    UserActionType.SIGN_IN_COMPLETED,  // provide custom success action\n    UserActionType.SIGN_IN_FAILED,     // provide custom error action\n  )\n]\n```\n\nAPI function will receive two parameters: \n\n- `action`: Payload portion of the action (eg: `{ type: 'SIGN_IN_USER', payload: {email: 'test@mail.com' }}` - action will be `{ email: 'test@mail.com'}` } )\n\n- `state` - Current redux state\n\n```js\nexport function signIn(action: { email: string }, state: ReduxState) {\n  // your api code\n}\n```\n\n### `createSagaForEvery(action: string, api: Function, successAction?: string, failureAction?: string)`\n\nThis works just like `createSaga`, but uses `takeEvery` in place of `takeLatest`. This function is best suited for handling simultaneous actions without cancelling the previous one.\n\n```ts\nimport { createSagaForEvery } from 'redux-saga-tools'\nimport { UserActionType } from './user-actions'\n\nexport default [\n  createSagaForEvery(\n    UserActionType.SIGN_IN_USER,\n    userApi.signIn,\n  )\n]\n\n// will emit SIGN_IN_USER_SUCCESS or SIGN_IN_USER_ERROR\n```\n\n### `createSagaStream(api: Function, sagaOrAction: Function | string)`\n\nYou can open a channel to work with stream of data from the API. For example, the below snippet will emit `ON_CURRENT_USER_CHANGE` action whenever `onAuthChange` emits a value through `callback`.\n\n```ts\nimport { createSaga } from 'redux-saga-tools'\nimport { UserActionType } from './user-actions'\n\nexport default [\n  createSagaStream(\n    onAuthChange,\n    UserActionType.ON_CURRENT_USER_CHANGE\n  )\n]\n\nfunction onAuthChange (callback, action) {\n  return asyncAction((data) =\u003e callback(data))\n}\n```\n\n### `createSagaChannel(startAndEndActions: string | string[], callback: Function, sagaOrAction: Function | string): Function`\n\nThis works similar to `createSagaStream`, but opens a channel on an action and closes the channel on another action. End action is optional. For every repeated start action, the channel will be closed and reopened, thus keeping one active channel always.\n\n```ts\nimport { createSagaChannel } from 'redux-saga-tools'\nimport { UserActionType } from './user-actions'\nimport { TodoActionType } from './todo-actions'\n\nexport default [\n  createSagaChannel(\n    [UserActionType.SIGN_IN_USER_SUCCESS, UserActionType.SIGN_OUT_USER_SUCCESS]\n    watchTodoByUser,\n    TodoActionType.ON_CHANGE_TODO_BY_USER\n  )\n]\n\nfunction watchUserTodoByUser(callback: Function, { user }: { user: User }) {\n\n  const subscription =  asyncAction(user, (data) =\u003e callback(data))\n\n  return function unsubscribe() {\n    subscription.cancel()\n  }\n}\n```\n\nYour handler must return a function, that will be invoked just before the channel is closed. Therefore the returned function can be used to close any open subscriptions or API resources.\n\n### Start Saga\n\nPutting it all together.\n\n```js\nimport { all } from 'redux-saga/effects'\nimport { reducers } from './reducers'\nimport { userSagas } from './user/user-saga'\nimport { todoSagas } from './todo/todo-saga'\n\nexport function* sagas(): any {\n  yield all([\n    ...userSagas,\n    ...todoSagas,\n  ])\n}\n\n// configure middleware\nconst sagaMiddleware = createSagaMiddleware()\nconst middleware = applyMiddleware(sagaMiddleware)\n\n// create store\nexport const store = createStore(reducers, config.initialState, middleware)\n\n// run saga\nsagaMiddleware.run(sagas as any)\n```\n\n## 2. Action\n\nCreate actions using `ActionsUnion`, `ById` and `createAction` utilities to make actions fully compatible with TypeScript, and thereby code with strongly typed actions and types.\n\n```js\nimport { ActionsUnion, ById, createAction } from 'redux-saga-tools'\nimport { User } from './user'\n\nexport enum UserActionType {\n  FETCH_BY_ID = '@User/FETCH_BY_ID',\n  FETCH_BY_ID_SUCCESS = '@User/FETCH_BY_ID_SUCCESS',\n\n  ON_CURRENT_USER_CHANGE = '@User/ON_CURRENT_USER_CHANGE',\n\n  SIGN_IN = '@User/SIGN_IN',\n  SIGN_IN_SUCCESS = '@User/SIGN_IN_SUCCESS',\n\n  SIGN_OUT = '@User/SIGN_OUT',\n  SIGN_OUT_SUCCESS = '@User/SIGN_OUT_SUCCESS',\n}\n\nexport const UserActions = {\n  fetchById: (id: string) =\u003e createAction(UserActionType.FETCH_BY_ID, { id }),\n\n  signIn: (email: string) =\u003e createAction(UserActionType.SIGN_IN, { email }),\n  signOut: () =\u003e createAction(UserActionType.SIGN_OUT),\n}\n\nexport type UserActions = ActionsUnion\u003ctypeof UserActions\u003e\n```\n\nDispatch an action:\n\n```js\ndispatch(UserActions.fetchById('id'))\n```\n\n## 3. Reducer\n\nThis section contains utility functions that will accelerate the development of reducer functions.\n\n### `createReducer(initialState: any, handlers: any)`\n\nSwitch statements are generally used for writing reducer functions. `createReducer` will allow you to attach an action with a reducer through an object map instead. This will help you write every reducer as a functional unit with its own variables and parameters, making it easy to test and maintain.\n\n```ts\n// user-reducer.js\n\nimport { createReducer } from 'redux-saga-tools'\n\nfunction onUserSignIn(state: UserState, action: { user: User}): UserState {\n  return { ...state, currentUser: action.user }\n}\n\nfunction onUserSignOut(state: UserState): UserState {\n  return { ...state, currentUser: undefined }\n}\n\nexport default createReducer({}, {\n  [UserActionType.SIGN_IN_USER_SUCCESS]: onUserSignIn,\n  [UserActionType.SIGN_OUT_USER_SUCCESS]: onUserSignOut,\n})\n```\n\nUse `combineReducers` to create root reducer.\n\n```js\n// reducers.js\n\nimport { combineReducers } from 'redux'\nimport { progressReducer } from 'redux-saga-tools'\nimport todoReducer from './todo/todo-reducer'\nimport userReducer from './user/user-reducer'\n\nexport const reducers = combineReducers({\n  user: userReducer,\n  todo: todoReducer,\n  progress: progressReducer,\n})\n```\n\n#### Progress Reducer\n\nPlease note the special reducer `progressReducer`. We will discuss about this [later in this article](#handling-action-progress).\n\n### `reduceById(array: T[]): ById\u003cT\u003e`\n\nReduces an array of object with property `id` to an object with keys as `id`\n\n```js\nconst users = [\n  {id: 'a', name: 'John'  }\n  {id: 'b', name: 'Jack'  }\n]\nconst usersById = reduceById(users)  \n\n// will return  \n// {\n//   a:  {id: 'a', name: 'John'  },\n//   b:  {id: 'b', name: 'Jack'  }\n// }\n```\n\n### `toArray(byId: ById\u003cT\u003e): T[]`\n\nConverts an object by id into an array (reverse operation of `reduceById`)\n\n```js\nconst usersById = {\n  a:  {id: 'a', name: 'John'  },\n  b:  {id: 'b', name: 'Jack'  }\n}\nconst users = toArray(usersById)  \n\n// will return\n// [\n//   {id: 'a', name: 'John'  }\n//   {id: 'b', name: 'Jack'  }\n// ]\n```\n\n### `setById\u003cT\u003e(state: any, byId: ById\u003cT\u003e): any`\n\nKeeping core objects in state by id is a common practice in redux. This function will do the merge as required.\n\n```js\nfunction reduceStateOnUsersChange(userState: UserState, usersById: ById\u003cUser\u003e) {\n  return setById(userState, usersById)\n}\n\n// will return:\n// {\n//   ...userState,\n//   byId: {\n//     ...userState.byId,\n//     ...byId\n//   }\n// }\n```\n\n### `filterById(byId: ById\u003cT\u003e, ids: string[]): ById\u003cT\u003e`\n\nThis function will return an object with `id` matching one of the values in `ids`.\n\n```js\nconst usersById = {\n  a:  {id: 'a', name: 'John'  },\n  b:  {id: 'b', name: 'Jack'  }\n}\nconst users = filterById(usersById, ['b'])  \n\n// will return:\n// {\n//   b:  {id: 'b', name: 'Jack'  }\n// }\n```\n\n### `filterToArrayById(byId: ById\u003cT\u003e, ids: string[]): T[]`\n\nThis works similar to `filterById`, returns an array of matching items.\n\n```js\nconst usersById = {\n  a:  {id: 'a', name: 'John'  },\n  b:  {id: 'b', name: 'Jack'  }\n}\nconst users = filterToArrayById(usersById, ['b'])  \n\n// will return:\n// [\n//   {id: 'b', name: 'Jack'  }\n// ]\n```\n\n### `unique(array?: any[])`\n\nThis function will return unique items of an array.\n\n```js\nconst ids = unique(['a', 'b', 'b', 'c']) // ['a', 'b', 'c']\n```\n\n### `uniqueProps(array?: any[], property: string = 'id'): any[]`\n\nThis function will return unique values of a property from the given array of objects.\n\n```js\nconst ids = uniqueProps([{ id: 'a' }, { id: 'b' }, { id: 'b' }, { id: 'c' }], 'id') // ['a', 'b', 'c']\n```\n\n### `uniquePropsById(byId?: ById\u003cany\u003e, property: string = 'id'): any[]`\n\nSame as `uniqueProps`, but works with an object instead of an array.\n\n```js\nconst ids = uniquePropsById({\n  'a1': { id: 'a' },\n  'a2': { id: 'b' },\n  'a3': { id: 'b' },\n  'a4': { id: 'c' }\n}, 'id') // ['a', 'b', 'c']\n```\n\n### `missingIds(byId: ById\u003cT\u003e, ids: string[])`\n\nThis function will return the list of ids for which the values are missing in `byId`\n\n```js\nconst ids = missingIds({\n  'a1': { id: 'a' },\n  'a2': { id: 'b' },\n  'a3': { id: 'b' },\n  'a4': { id: 'c' }\n}, ['a1', 'a5', 'a6']) // ['a5', 'a6']\n```\n\n## Handling Action Progress\n\nUI needs to know the progress of the action being carried out by the saga or API to respond gracefully to users. This library is configured with a progress tracking setup.\n\nTo track progress of an action, use `selectProgress` by providing current application state as the first parameter and the action you want to track as the second parameter. Remember to add `progressReducer` to the root reducer for this setup to work ([See here](#progress-reducer))\n\n```tsx\nimport * as React from 'react'\nimport { didProgressComplete, didProgressFail, Progress, selectProgress } from 'redux-saga-tools'\n\nexport interface Props {\n  ...\n  dispatch?: Dispatch\u003cany\u003e\n  todoId?: string\n  todoProgress?: Progress\n}\n\nexport interface State { }\n\nclass TodoScreen extends React.Component\u003cProps, State\u003e {\n\n  componentWillReceiveProps(props: Props) {\n    if (didProgressComplete(props.todoProgress, this.props.todoProgress)) {\n      // take next action in UI\n    } else if (didProgressFail(props.todoProgress, this.props.todoProgress)) {\n      // show an error\n    }\n  }\n\n  fetch() {\n    const { dispatch, todoId } = this.props\n    dispatch \u0026\u0026 dispatch(TodoActions.fetchTodo(todoId))\n  }\n\n  render() {\n    return \u003cdiv\u003e\n      {todoProgress \u0026\u0026 todoProgress.inProgress \u0026\u0026 \u003cdiv\u003eLoading...\u003c/div\u003e}\n      ...\n    \u003c/div\u003e\n  }\n}\n\nfunction mapStateToProps(state: AppState): Props {\n  return {\n    ...\n    todoProgress: selectProgress(state, TodoActionType.FETCH_TODO_BY_ID),\n  }\n}\n\nexport default connect(mapStateToProps)(TodoScreen)\n```\n\n## About\n\n### Hope this library is helpful to you. Please make sure to checkout my other [projects](https://github.com/rintoj) and [articles](https://medium.com/@rintoj). Enjoy coding!\n\n## Contributing\n\nContributions are welcome! Just send a pull request. Feel free to contact [me](mailto:rintoj@gmail.com) or checkout my [GitHub](https://github.com/rintoj) page.\n\n## Author\n\n**Rinto Jose** (rintoj)\n\nFollow me:\n  [GitHub](https://github.com/rintoj)\n| [Facebook](https://www.facebook.com/rinto.jose)\n| [Twitter](https://twitter.com/rintoj)\n| [Youtube](https://youtube.com/+RintoJoseMankudy)\n\n## License\n\n```code\nThe MIT License (MIT)\n\nCopyright (c) 2019 Rinto Jose (rintoj)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frintoj%2Fredux-saga-tools","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frintoj%2Fredux-saga-tools","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frintoj%2Fredux-saga-tools/lists"}