{"id":15465885,"url":"https://github.com/brianegan/dart_redux_epics","last_synced_at":"2025-04-12T18:53:07.774Z","repository":{"id":44776311,"uuid":"70352590","full_name":"brianegan/dart_redux_epics","owner":"brianegan","description":"Redux.dart middleware for handling actions using Dart Streams","archived":false,"fork":false,"pushed_at":"2025-02-11T11:55:11.000Z","size":79,"stargazers_count":139,"open_issues_count":0,"forks_count":22,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-04-03T22:08:17.151Z","etag":null,"topics":["dart","dart-streams","epics","redux","redux-epics","rx"],"latest_commit_sha":null,"homepage":"","language":"Dart","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/brianegan.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":"2016-10-08T19:21:23.000Z","updated_at":"2025-02-17T15:00:35.000Z","dependencies_parsed_at":"2025-01-15T02:05:30.584Z","dependency_job_id":"15196ac5-6ab9-40b4-8e29-a7f0cdeba423","html_url":"https://github.com/brianegan/dart_redux_epics","commit_stats":{"total_commits":53,"total_committers":15,"mean_commits":3.533333333333333,"dds":0.5849056603773585,"last_synced_commit":"a5be7de2374f63f69c1c0c0be90176da7be03397"},"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brianegan%2Fdart_redux_epics","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brianegan%2Fdart_redux_epics/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brianegan%2Fdart_redux_epics/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brianegan%2Fdart_redux_epics/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/brianegan","download_url":"https://codeload.github.com/brianegan/dart_redux_epics/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248618243,"owners_count":21134200,"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":["dart","dart-streams","epics","redux","redux-epics","rx"],"created_at":"2024-10-02T01:04:05.115Z","updated_at":"2025-04-12T18:53:07.752Z","avatar_url":"https://github.com/brianegan.png","language":"Dart","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Redux Epics \n\n[![CI Checks](https://github.com/brianegan/dart_redux_epics/actions/workflows/health_checks.yaml/badge.svg)](https://github.com/brianegan/dart_redux_epics/actions/workflows/health_checks.yaml)\n\n[Redux](https://pub.dartlang.org/packages/redux) is great for synchronous\nupdates to a store in response to actions. However, working with complex\nasynchronous operations, such as autocomplete search experiences, can be a bit\ntricky with traditional middleware. This is where Epics come in!\n\nThe best part: Epics are based on Dart Streams. This makes routine tasks easy,\nand complex tasks such as asynchronous error handling, cancellation, and\ndebouncing a breeze.\n\nNote: For users unfamiliar with Streams, simple async cases are easier to handle\nwith a normal Middleware Function. If normal Middleware Functions or \n[Thunks](https://pub.dartlang.org/packages/redux_thunk) work for you, you're\ndoing it right!  When you find yourself dealing with more complex scenarios,\nsuch as writing an Autocomplete UI, check out the Recipes below to see how\nStreams / Epics can make your life easier.\n\n## Example\n\nLet's say your app has a search box. When a user submits a search term, you\ndispatch a `PerformSearchAction` which contains the term. In order to actually\nlisten for the `PerformSearchAction` and make a network request for the results,\nwe can create an Epic!\n\nIn this instance, our Epic will need to filter all incoming actions it receives\nto only the `Action` it is interested in: the `PerformSearchAction`. This will\nbe done using the `where` method on Streams. Then, we need to make a network\nrequest with the search term using `asyncMap` method. Finally, we need to\ntransform those results into an action that contains the search results. If an\nerror has occurred, we'll want to return an error action so our app can respond\naccordingly.\n\nHere's what the above description looks like in code.\n\n```dart\nimport 'dart:async';\nimport 'package:redux_epics/redux_epics.dart';\n\nStream\u003cdynamic\u003e exampleEpic(Stream\u003cdynamic\u003e actions, EpicStore\u003cState\u003e store) {\n  return actions\n    .where((action) =\u003e action is PerformSearchAction)\n    .asyncMap((action) =\u003e \n      // Pseudo api that returns a Future of SearchResults\n      api.search((action as PerformSearch).searchTerm)\n        .then((results) =\u003e SearchResultsAction(results))\n        .catchError((error) =\u003e SearchErrorAction(error)));\n}\n```\n\n### Connecting the Epic to the Redux Store\n\nNow that we've got an epic to work with, we need to wire it up to our Redux\nstore so it can receive a stream of actions. In order to do this, we'll employ\nthe `EpicMiddleware`.\n\n```dart\nimport 'package:redux_epics/redux_epics.dart';\nimport 'package:redux/redux.dart';\n\nvar epicMiddleware = new EpicMiddleware(exampleEpic);\nvar store = new Store\u003cState\u003e(fakeReducer, middleware: [epicMiddleware]);\n```\n### Combining epics and normal middleware\n\nTo combine the epic Middleware and normal middleware, simply use both in the \nlist! Note: You may need to provide  \n\n```dart\nvar store = new Store\u003cAppState\u003e(\n  fakeReducer,\n  middleware: [myMiddleware, EpicMiddleware\u003cAppState\u003e(exampleEpic)],\n);\n```\n\nIf you're combining two Lists, please make sure to use the `+` or the `...` \nspread operator. \n\n```dart\nvar store = new Store\u003cAppState\u003e(\n  fakeReducer,\n  middleware: [myMiddleware] + [EpicMiddleware\u003cAppState\u003e(exampleEpic)],\n);\n```\n\n## Combining Epics\n\nRather than having one massive Epic that handles every possible type of action,\nit's best to break Epics down into smaller, more manageable and testable units.\nThis way we could have a `searchEpic`, a `chatEpic`, and an `updateProfileEpic`,\nfor example.\n\nHowever, the `EpicMiddleware` accepts only one Epic. So what are we to do? Fear\nnot: redux_epics includes class for combining Epics together!\n\n```dart\nimport 'package:redux_epics/redux_epics.dart';\nfinal epic = combineEpics\u003cState\u003e([\n  searchEpic, \n  chatEpic, \n  updateProfileEpic,\n]);\n```\n\n## Advanced Recipes\n\nIn order to perform more advanced operations, it's often helpful to use a\nlibrary such as [RxDart](https://github.com/ReactiveX/rxdart).\n\n### Casting\n\nIn order to use this library effectively, you generally need filter down to\nactions of a certain type, such as `PerformSearchAction`. In the previous\nexamples, you'll noticed that we need to filter using the `where` method on the\nStream, and then manually cast (`action as SomeType`) later on.\n\nTo more conveniently narrow down actions to those of a certain type, you have\ntwo options:\n\n### TypedEpic\n\nThe first option is to use the built-in `TypedEpic` class. This will allow you\nto write Epic functions that handle actions of a specific type, rather than all\nactions!\n\n```dart\nfinal epic = new TypedEpic\u003cState, PerformSearchAction\u003e(searchEpic);\n\nStream\u003cdynamic\u003e searchEpic(\n  // Note: This epic only handles PerformSearchActions\n  Stream\u003cPerformSearchAction\u003e actions, \n  EpicStore\u003cState\u003e store,\n) {\n  return actions\n    .asyncMap((action) =\u003e\n      // No need to cast the action to extract the search term!\n      api.search(action.searchTerm)\n        .then((results) =\u003e SearchResultsAction(results))\n        .catchError((error) =\u003e SearchErrorAction(error)));\n}\n```\n\n#### RxDart \n\nYou can use the `whereType` method provided by RxDart. It will both perform a\n`where` check and then cast the action for you.\n\n```dart\nimport 'package:redux_epics/redux_epics.dart';\nimport 'package:rxdart/rxdart.dart';\n\nStream\u003cdynamic\u003e ofTypeEpic(Stream\u003cdynamic\u003e actions, EpicStore\u003cState\u003e store) {\n  // Wrap our actions Stream as an Observable. This will enhance the stream with\n  // a bit of extra functionality.\n  return actions\n    // Use `whereType` to narrow down to PerformSearchAction \n    .whereType\u003cPerformSearchAction\u003e()\n    .asyncMap((action) =\u003e\n      // No need to cast the action to extract the search term!\n      api.search(action.searchTerm)\n        .then((results) =\u003e SearchResultsAction(results))\n        .catchError((error) =\u003e SearchErrorAction(error)));\n}\n```  \n\n### Cancellation\n\nIn certain cases, you may need to cancel an asynchronous task. For example, your\napp begins loading data in response to a user clicking on a the search button by\ndispatching a `PerformSearchAction`, and then the user hit's the back button in\norder to correct the search term. In that case, your app dispatches a\n`CancelSearchAction`. We want our `Epic` to cancel the previous search in\nresponse to the action. So how can we accomplish this?\n\nThis is where Observables really shine. In the following example, we'll employ\nObservables from the RxDart library to beef up the power of streams a bit, using\nthe `switchMap` and `takeUntil` operator.\n\n```dart\nimport 'package:redux_epics/redux_epics.dart';\nimport 'package:rxdart/rxdart.dart';\n\nStream\u003cdynamic\u003e cancelableSearchEpic(\n  Stream\u003cdynamic\u003e actions,\n  EpicStore\u003cState\u003e store,\n) {\n  return actions\n      .whereType\u003cPerformSearchAction\u003e()\n      // Use SwitchMap. This will ensure if a new PerformSearchAction\n      // is dispatched, the previous searchResults will be automatically \n      // discarded.\n      //\n      // This prevents your app from showing stale results.\n      .switchMap((action) {\n        return Stream.fromFuture(api.search(action.searchTerm)\n            .then((results) =\u003e SearchResultsAction(results))\n            .catchError((error) =\u003e SearchErrorAction(error)))\n            // Use takeUntil. This will cancel the search in response to our\n            // app dispatching a `CancelSearchAction`.\n            .takeUntil(actions.whereType\u003cCancelSearchAction\u003e());\n  });\n}\n```\n\n### Autocomplete using debounce\n\nLet's take this one step further! Say we want to turn our previous example into\nan Autocomplete Epic. In this case, every time the user types a letter into the\nText Input, we want to fetch and show the search results. Each time the user\ntypes a letter, we'll dispatch a `PerformSearchAction`.\n\nIn order to prevent making too many API calls, which can cause unnecessary load\non your backend servers, we don't want to make an API call on every single\n`PerformSearchAction`. Instead, we'll wait until the user pauses typing for a\nshort time before calling the backend API.\n\nWe'll achieve this using the `debounce` operator from RxDart.\n\n```dart\nimport 'package:redux_epics/redux_epics.dart';\nimport 'package:rxdart/rxdart.dart';\n\nStream\u003cdynamic\u003e autocompleteEpic(\n  Stream\u003cdynamic\u003e actions,\n  EpicStore\u003cState\u003e store,\n) {\n  return actions\n      .whereType\u003cPerformSearchAction\u003e()\n      // Using debounce will ensure we wait for the user to pause for \n      // 150 milliseconds before making the API call\n      .debounce(new Duration(milliseconds: 150))\n      .switchMap((action) {\n        return Stream.fromFuture(api.search(action.searchTerm)\n                .then((results) =\u003e SearchResultsAction(results))\n                .catchError((error) =\u003e SearchErrorAction(error)))\n            .takeUntil(actions.whereType\u003cCancelSearchAction\u003e());\n  });\n}\n```\n\n## Dependency Injection\n\nDependencies can be injected manually with either a Functional or an Object-Oriented style. If you choose, you may use a Dependency Injection or Service locator library as well.\n\n### Functional\n```dart\n// epic_file.dart\nEpic\u003cAppState\u003e createEpic(WebService service) {\n  return (Stream\u003cdynamic\u003e actions, EpicStore\u003cAppState\u003e store) async* {\n    service.doSomething()...\n  }\n}\n```\n\n### OO\n```dart\n// epic_file.dart\nclass MyEpic implements EpicClass\u003cState\u003e {\n  final WebService service;\n\n  MyEpic(this.service);\n\n  @override\n  Stream\u003cdynamic\u003e call(Stream\u003cdynamic\u003e actions, EpicStore\u003cState\u003e store) {\n    service.doSomething()...\n  } \n}\n```\n\n#### Usage - Production\nIn production code the epics can be created at the point where `combineEpics` is called. If you're using separate `main_\u003cenvironment\u003e.dart` files to [configure your application for different environments](https://stackoverflow.com/questions/47438564/how-do-i-build-different-versions-of-my-flutter-app-for-qa-dev-prod) you may want to pass the config to the `RealWebService` at this point.\n\n```dart\n// app_store.dart\nimport 'package:epic_file.dart';\n...\n\nfinal apiBaseUrl = config.apiBaseUrl\n\nfinal functionalEpic = createEpic(new RealWebService(apiBaseUrl));\n// or\nfinal ooEpic = new MyEpic(new RealWebService(apiBaseUrl));\n\nstatic final epics = combineEpics\u003cAppState\u003e([\n    functionalEpic,\n    ooEpic,    \n    ...\n    ]);\nstatic final epicMiddleware = new EpicMiddleware(epics);\n```\n\n#### Usage - Testing\n```dart\n...\nfinal testFunctionalEpic = createEpic(new MockWebService());\n// or\nfinal testOOEpic = new MyEpic(new MockWebService());\n...\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbrianegan%2Fdart_redux_epics","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbrianegan%2Fdart_redux_epics","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbrianegan%2Fdart_redux_epics/lists"}