{"id":16019468,"url":"https://github.com/dwindler/dwindler","last_synced_at":"2025-04-05T03:24:48.468Z","repository":{"id":57217450,"uuid":"117729331","full_name":"dwindler/dwindler","owner":"dwindler","description":"Simple and boilerplate free redux module factory","archived":false,"fork":false,"pushed_at":"2018-02-02T12:19:37.000Z","size":248,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-03-14T06:11:09.182Z","etag":null,"topics":["ducks-pattern","es6","javascript","redux","state-management"],"latest_commit_sha":null,"homepage":"","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/dwindler.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-01-16T19:14:45.000Z","updated_at":"2018-02-07T13:21:18.000Z","dependencies_parsed_at":"2022-08-28T21:41:18.551Z","dependency_job_id":null,"html_url":"https://github.com/dwindler/dwindler","commit_stats":null,"previous_names":["ilkkahanninen/dwindler"],"tags_count":8,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dwindler%2Fdwindler","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dwindler%2Fdwindler/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dwindler%2Fdwindler/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dwindler%2Fdwindler/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dwindler","download_url":"https://codeload.github.com/dwindler/dwindler/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247283044,"owners_count":20913489,"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":["ducks-pattern","es6","javascript","redux","state-management"],"created_at":"2024-10-08T17:04:29.628Z","updated_at":"2025-04-05T03:24:48.441Z","avatar_url":"https://github.com/dwindler.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Dwindler\n\nDwindler is a simple and boilerplate free [Redux](https://redux.js.org) module bundle factory for Javascript.\n\n## Installation\n\n`npm install dwindler redux --save`\n\nor\n\n`yarn add dwindler redux`\n\n## Motivation\n\nRedux is great and makes your software robust but using it is a little too cumbersome and enterprisey. Dwindler easies the pain by defining type names automatically and simplifying the reducer composition.\n\nDwindler puts related actions, reducer and initial state inside the same entity and therefore it is inspired from the [ducks pattern](https://github.com/erikras/ducks-modular-redux). It's action creator pattern is inspired from [thunks](https://github.com/gaearon/redux-thunk).\n\nDwindler **does not** store the state or send events; It is designed to work alongside with Redux. This makes it possible to use the great tools and middleware created for Redux, such as [DevTools extension](https://github.com/zalmoxisus/redux-devtools-extension).\n\nDwindler does not take care of immutability either. I recommend to use [seamless-immutable](https://github.com/rtfeldman/seamless-immutable), but nothing stops you from using [Immutable.js](https://facebook.github.io/immutable-js/) or plain good old JS objects with [Ramda](http://ramdajs.com/), [Lodash/fp](https://github.com/lodash/lodash/wiki/FP-Guide) or spread operators (as used in the examples).\n\n## Usage\n\nConceptually store is a tree structure in Dwindler. Each node is a module which contains its own properties holding the application state, action creators and reducers. You create your tree by implementing its nodes (both leaves and branches) as follows:\n\n```javascript\nconst counter = {\n  // Initial state\n  state: {\n    value: 0\n  },\n\n  // Action creators\n  actions: {\n    increase(amount = 1) {\n      this.dispatch('increaseValue', amount);\n    },\n    reset() {\n      this.setState({ value: 0 });\n    }\n  },\n\n  // Reducers\n  reducers: {\n    increaseValue: (state, amount) =\u003e ({\n      value: state.value + amount\n    })\n  }\n}\n```\n\nAbove we declared a node named `counter`. It has simple state with one property, one action creator to increase its value and a reducer to do the actual work.\n\nThis should be familiar if you are familiar with Redux and ducks. If you are not I recommend to read [the tutorial](https://redux.js.org/docs/basics/).\n\nThere are few differences compared to traditional Redux patterns:\n\n* State is always an object.\n  * This limitation is due to automatic type name generation.\n  * As types are automatically generated you don't have to care that the action types are unique among all the reducers.\n* Action creators do not return actions. The always call `this.dispatch()`.\n  * Action creators are similiar to thunks but instead of receiving `dispatch` and `getState` as arguments they have those functions bound to `this`.\n  * You may find this (pun intended) ugly among all the functional programming enthusiasm but this is considered option to make the code easier to write and reason.\n  * Don't worry, there exists a way to easily unit test your action creators.\n* `this.dispatch()` takes two arguments: `type` and `payload`.\n  * This makes sure that the actions have correct shape and it is also part of the automatic type name mapping.\n  * You can still dispatch standard Redux action by dispatching an action object, e.g. `dispatch({ type: 'MY_OWN_ACTION' })`\n\nLet's assumme the `counter` we declared above isn't alone and we have created also few other nodes (in this case `user` and `posts`). Let's wrap them together to a root node:\n\n```javascript\nconst root = {\n  children: {\n    counter,\n    user,\n    posts\n  }\n};\n```\n\nThis `root` could also have state, action creators and reducers but now it is going to be a simple wrapper. `children` property defines all child nodes and those nodes could potentially have their own child nodes. This forms a tree structure. There is no limit for the depth of the tree other than usability.\n\nNow we are ready to create our Redux store:\n\n```javascript\nimport { bundle } from 'dwindler';\nimport { createStore } from 'redux';\nimport root from './root';\n\nconst app = bundle(root);\nconst store = createStore(app.reducer);\nconst actions = app.getActions(store);\n```\n\n`bundle()` takes the root node and composes a reducer function and maps type names. It returns an object which contains two functions:\n\n* `reducer()` is the composed reducer function for `createStore`.\n* `getActions()` binds store to action creators and returns a tree of bound action creators.\n\nNow we can test our brand new store:\n\n```javascript\n// Call counter's action creator\nactions.counter.increase();\n\n// Get state and print counter's value to the console\nconst state = store.getState();\nconsole.log(state.counter.value); // 1\n```\n\nAs you can see the `state` and `actions` both follow the tree structure and naming. If you have some devtools installed they would have noticed the following action being dispatched:\n\n```json\n{\n  \"type\": \"counter/increaseValue\",\n  \"payload\": 1\n}\n```\n\nFormat for type names is `path.to.node/dispatchType`.\n\n### External data sources\n\nNo application is an island and you most probably need to fetch data from API or other external service. Dwindler provides a standard way to inject these dependencies as services to action creators. This is a recommended way because if you need to mock your API calls for unit tests you can simply provide a mock version instead of a real thing.\n\nProvide your services in the second argument for `bundle(root, options)` with property `services`. Inside action creators you can access the services in `this.services`.\n\nAs an example let's use something [Axios](https://github.com/axios/axios) as a simple REST API service:\n\n```javascript\nimport { bundle } from 'dwindler';\nimport axios from 'axios';\nimport root from './root';\n\naxios.defaults.baseURL = 'https://jsonplaceholder.typicode.com';\n\nconst services = {\n  backend: axios\n};\n\nconst app = bundle(root, { services });\n```\n\nNow we can have something like this somewhere in our action creators:\n\n```javascript\nconst posts = {\n  state: {\n    posts: []\n  },\n\n  actions: {\n    async getPosts() {\n      const posts = await this.services.backend.get('/posts');\n      this.dispatch('receivedPosts', posts);\n    }\n  },\n\n  reducers: {\n    receivedPosts: (state, posts) =\u003e ({ posts })\n  }\n};\n```\n\n### Using with React\n\nDwindler takes away the need to bind action creators to store in `connect()`. You can connect the actions in *mapStateToProps* argument.\n\nIt is still the best practise to bring the action creators in as props as it makes unit testing the component easy. **Do not** call the action creators directly from your component (e.g. `onClick={actions.logout}`).\n\n```javascript\nimport React from 'react';\nimport { connect } from 'react-redux';\nimport { actions } from './store';\n\nexport const UserBadge = ({ username, logout }) =\u003e (\n  \u003cdiv className=\"UserBadge\"\u003e\n    {username}\n    \u003cbutton onClick={logout}\u003e\n      Logout\n    \u003c/button\u003e\n  \u003c/div\u003e\n);\n\nexport default connect(\n  state =\u003e ({\n    // State\n    username: state.user.username,\n    // Actions\n    logout: actions.user.logout,\n  })\n)(UserBadge);\n```\n\n### Unit testing\n\nDwindler provides `testHarness(description, options)` function to test your bundles easily. It returns an object which contains bound versions of action creators and following methods:\n\n- `expectAction(type, payload, finalState)` defines an expected action from the action creator. If an argument is undefined/null it will not be tested so you can test actions only partially.\n- `dispatch(type, payload)` dispatches an action and returns the new state.\n- `getErrors()` returns an array of error strings from the validation. If there is no errors this array is empty.\n- `hasErrors()` returns true if there is one or more errors.\n- `actions` contains all bundle's action creators.\n\nThis example unit test uses [Tape](https://github.com/substack/tape) but you\nshould be able to use `testHarness()` with any JS testing framework.\n\n```javascript\nconst test = require('tape');\nconst { bundle, testHarness } = require('dwindler');\nconst user = require('./user');\n\ntest('Updating user bundle state works', t =\u003e {\n  const mockUserData = {\n    name: 'mary',\n    email: 'mary@email.com'\n  };\n\n  const getUserAPIMock = {\n    api: {\n      get() {\n        return mockUserData;\n      }\n    }\n  };\n\n  // Create harness with mocked API which returns an user\n  const userTest = testHarness(user, { services: getUserAPIMock });\n\n  // Expected actions\n  userTest.expectAction(\n    'getUserStarted', // Expected action type\n    null,             // null -\u003e Don't care about payload\n    {                 // Expected state after reducer\n      isLoading: true,\n      name: null,\n      email: null\n    }\n  );\n  userTest.expectAction(\n    'getUserSuccessful',  // Expected action type\n    mockUserData,         // Expected payload\n    {                     // Expected state after reducer\n      isLoading: false,\n      ...mockUserData\n    }\n  );\n\n  // Run action creators\n  userTest.actions.getUser(123);\n  t.equal(userTest.getErrors(), []); // We should not have errors\n\n  // Test nameChanged mutation independently\n  t.deepEqual(\n    userTest.dispatch('nameChanged', 'John'),   // Dispatch action\n    { name: 'John', email: 'mary@email.com' },  // Expected state after\n    'reducers.nameChanged works'\n  );\n\n  t.end();\n});\n```\n\n### Composing declarations\n\n```javascript\nimport { composeDeclarations } from 'dwindler';\n\n// This is a partial declaration which can be composed to\n// another declaration. It makes the bundle nameable.\nconst nameable = {\n  state: {\n    name: null\n  },\n  actions: {\n    setName(name) {\n      this.dispatch('setName', name);\n    }\n  },\n  reducers: {\n    setName: (state, name) =\u003e { ...state, name }\n  }\n};\n\nconst user = bundle(composeDeclarations(\n  nameable,\n  {\n    state: {\n      id: null,\n    }\n  },\n))\n```\n\n### Classic redux reducers\n\nYou can add standard Redux style reducer to the declation. This is the only way to catch actions dispatched from other nodes, middlewares or possible other sources.\n\n```javascript\nconst initialState = {\n  name: null\n};\n\nconst user = bundle({\n  state: initialState,\n\n  reducer(state, action) {\n    switch (action.type) {\n      case 'auth/logout':\n        return initialState;\n      default:\n        return state;\n    }\n  }\n})\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdwindler%2Fdwindler","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdwindler%2Fdwindler","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdwindler%2Fdwindler/lists"}