{"id":13727296,"url":"https://github.com/davidkpiano/useEffectReducer","last_synced_at":"2025-05-07T22:31:01.128Z","repository":{"id":42881983,"uuid":"253873691","full_name":"davidkpiano/useEffectReducer","owner":"davidkpiano","description":"useReducer + useEffect = useEffectReducer","archived":true,"fork":false,"pushed_at":"2023-01-06T03:30:37.000Z","size":753,"stargazers_count":790,"open_issues_count":21,"forks_count":24,"subscribers_count":7,"default_branch":"master","last_synced_at":"2025-04-21T16:53:11.759Z","etag":null,"topics":["hacktoberfest","hook","react","reducer","state"],"latest_commit_sha":null,"homepage":"","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/davidkpiano.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}},"created_at":"2020-04-07T18:04:16.000Z","updated_at":"2025-03-19T21:05:52.000Z","dependencies_parsed_at":"2023-02-05T05:32:04.162Z","dependency_job_id":null,"html_url":"https://github.com/davidkpiano/useEffectReducer","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/davidkpiano%2FuseEffectReducer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/davidkpiano%2FuseEffectReducer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/davidkpiano%2FuseEffectReducer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/davidkpiano%2FuseEffectReducer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/davidkpiano","download_url":"https://codeload.github.com/davidkpiano/useEffectReducer/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252965305,"owners_count":21832866,"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":["hacktoberfest","hook","react","reducer","state"],"created_at":"2024-08-03T01:03:48.660Z","updated_at":"2025-05-07T22:31:00.718Z","avatar_url":"https://github.com/davidkpiano.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"# useEffectReducer\n\nA [React hook](https://reactjs.org/docs/hooks-intro.html) for managing side-effects in your reducers.\n\nInspired by the [`useReducerWithEmitEffect` hook idea](https://gist.github.com/sophiebits/145c47544430c82abd617c9cdebefee8) by [Sophie Alpert](https://twitter.com/sophiebits).\n\nIf you know how to [`useReducer`](https://reactjs.org/docs/hooks-reference.html#usereducer), you already know how to `useEffectReducer`.\n\n[💻 CodeSandbox example: Dog Fetcher with `useEffectReducer`](https://codesandbox.io/s/dog-fetcher-with-useeffectreducer-g192g)\n\n\u003c!-- START doctoc generated TOC please keep comment here to allow auto update --\u003e\n\u003c!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --\u003e\n\n\n- [Installation](#installation)\n- [Quick Start](#quick-start)\n- [Named Effects](#named-effects)\n- [Effect Implementations](#effect-implementations)\n- [Initial Effects](#initial-effects)\n- [Effect Entities](#effect-entities)\n- [Effect Cleanup](#effect-cleanup)\n- [Replacing Effects](#replacing-effects)\n- [String Events](#string-events)\n- [API](#api)\n  - [`useEffectReducer` hook](#useeffectreducer-hook)\n  - [`exec(effect)`](#execeffect)\n  - [`exec.stop(entity)`](#execstopentity)\n- [`exec.replace(entity, effect)`](#execreplaceentity-effect)\n- [TypeScript](#typescript)\n\n\u003c!-- END doctoc generated TOC please keep comment here to allow auto update --\u003e\n\n## Installation\n\nInstall it:\n\n```bash\nnpm install use-effect-reducer\n```\n\nImport it:\n\n```js\nimport { useEffectReducer } from 'use-effect-reducer';\n```\n\nCreate an effect reducer:\n\n```js\nconst someEffectReducer = (state, event, exec) =\u003e {\n  // execute effects like this:\n  exec(() =\u003e {/* ... */});\n\n  // or parameterized (better):\n  exec({ type: 'fetchUser', user: event.user });\n\n  // and treat this like a normal reducer!\n  // ...\n\n  return state;\n};\n```\n\n[Use it:](#quick-start)\n\n```js\n// ...\nconst [state, dispatch] = useEffectReducer(someEffectReducer, initialState, {\n  // implementation of effects\n});\n\n// Just like useReducer:\ndispatch({ type: 'FETCH', user: 'Sophie' });\n```\n\n## Isn't this unsafe?\n\nNo - internally, `useEffectReducer` (as the name implies) is abstracting this pattern:\n\n```js\n// pseudocode\nconst myReducer = ([state], event) =\u003e {\n  const effects = [];\n  const exec = (effect) =\u003e effects.push(effect);\n  \n  const nextState = // calculate next state\n  \n  return [nextState, effects];\n}\n\n// in your component\nconst [[state, effects], dispatch] = useReducer(myReducer);\n\nuseEffect(() =\u003e {\n  effects.forEach(effect =\u003e {\n    // execute the effect\n  });\n}, [effects]);\n```\n\nInstead of being implicit about which effects are executed and _when_ they are executed, you make this explicit in the \"effect reducer\" with the helper `exec` function. Then, the `useEffectReducer` hook will take the pending effects and properly execute them within a `useEffect()` hook.\n\n## Quick Start\n\nAn \"effect reducer\" takes 3 arguments:\n\n1. `state` - the current state\n2. `event` - the event that was dispatched to the reducer\n3. `exec` - a function that captures effects to be executed and returns an [effect entity](#effect-entities) that allows you to control the effect\n\n```js\nimport { useEffectReducer } from 'use-effect-reducer';\n\n// I know, I know, yet another counter example\nconst countReducer = (state, event, exec) =\u003e {\n  switch (event.type) {\n    case 'INC':\n      exec(() =\u003e {\n        // \"Execute\" a side-effect here\n        console.log('Going up!');\n      });\n\n      return {\n        ...state,\n        count: state.count + 1,\n      };\n\n    default:\n      return state;\n  }\n};\n\nconst App = () =\u003e {\n  const [state, dispatch] = useEffectReducer(countReducer, { count: 0 });\n\n  return (\n    \u003cdiv\u003e\n      \u003coutput\u003eCount: {state.count}\u003c/output\u003e\n      \u003cbutton onClick={() =\u003e dispatch('INC')}\u003eIncrement\u003c/button\u003e\n    \u003c/div\u003e\n  );\n};\n```\n\n## Named Effects\n\nA better way to make reusable effect reducers is to have effects that are **named** and **parameterized**. This is done by running `exec(...)` an effect object (instead of a function) and specifying that named effect's implementation as the 3rd argument to `useEffectReducer(reducer, initial, effectMap)`.\n\n```js\nconst fetchEffectReducer = (state, event, exec) =\u003e {\n  switch (event.type) {\n    case 'FETCH':\n      // Capture a named effect to be executed\n      exec({ type: 'fetchFromAPI', user: event.user });\n\n      return {\n        ...state,\n        status: 'fetching',\n      };\n    case 'RESOLVE':\n      return {\n        status: 'fulfilled',\n        user: event.data,\n      };\n    default:\n      return state;\n  }\n};\n\nconst initialState = { status: 'idle', user: undefined };\n\nconst fetchFromAPIEffect = (_, effect, dispatch) =\u003e {\n  fetch(`/api/users/${effect.user}`)\n    .then(res =\u003e res.json())\n    .then(data =\u003e {\n      dispatch({\n        type: 'RESOLVE',\n        data,\n      });\n    });\n};\n\nconst Fetcher = () =\u003e {\n  const [state, dispatch] = useEffectReducer(fetchEffectReducer, initialState, {\n    // Specify how effects are implemented\n    fetchFromAPI: fetchFromAPIEffect,\n  });\n\n  return (\n    \u003cbutton\n      onClick={() =\u003e {\n        dispatch({ type: 'FETCH', user: 42 });\n      }}\n    \u003e\n      Fetch user\n    \u003c/div\u003e\n  );\n};\n```\n\n## Effect Implementations\n\nAn effect implementation is a function that takes 3 arguments:\n\n1. The `state` at the time the effect was executed with `exec(effect)`\n2. The `event` object that triggered the effect\n3. The effect reducer's `dispatch` function to dispatch events back to it. This enables dispatching within effects in the `effectMap` if it is written outside of the scope of your component. If your effects require access to variables and functions in the scope of your component, write your `effectMap` there.\n\nThe effect implementation should return a disposal function that cleans up the effect:\n\n```js\n// Effect defined inline\nexec(() =\u003e {\n  const id = setTimeout(() =\u003e {\n    // do some delayed side-effect\n  }, 1000);\n\n  // disposal function\n  return () =\u003e {\n    clearTimeout(id);\n  };\n});\n```\n\n```js\n// Parameterized effect implementation\n// (in the effect reducer)\nexec({ type: 'doDelayedEffect' });\n\n// ...\n\n// (in the component)\nconst [state, dispatch] = useEffectReducer(someReducer, initialState, {\n  doDelayedEffect: () =\u003e {\n    const id = setTimeout(() =\u003e {\n      // do some delayed side-effect\n    }, 1000);\n\n    // disposal function\n    return () =\u003e {\n      clearTimeout(id);\n    };\n  },\n});\n```\n\n## Initial Effects\n\nThe 2nd argument to `useEffectReducer(state, initialState)` can either be a static `initialState` or a function that takes in an effect `exec` function and returns the `initialState`:\n\n```js\nconst fetchReducer = (state, event) =\u003e {\n  if (event.type === 'RESOLVE') {\n    return {\n      ...state,\n      data: event.data,\n    };\n  }\n\n  return state;\n};\n\nconst getInitialState = exec =\u003e {\n  exec({ type: 'fetchData', someQuery: '*' });\n\n  return { data: null };\n};\n\n// (in the component)\nconst [state, dispatch] = useEffectReducer(fetchReducer, getInitialState, {\n  fetchData(_, { someQuery }) {\n    fetch(`/some/api?${someQuery}`)\n      .then(res =\u003e res.json())\n      .then(data =\u003e {\n        dispatch({\n          type: 'RESOLVE',\n          data,\n        });\n      });\n  },\n});\n```\n\n## Effect Entities\n\nThe `exec(effect)` function returns an **effect entity**, which is a special object that represents the running effect. These objects can be stored directly in the reducer's state:\n\n```js\nconst someReducer = (state, event, exec) =\u003e {\n  // ...\n\n  return {\n    ...state,\n    // state.someEffect is now an effect entity\n    someEffect: exec(() =\u003e {\n      /* ... */\n    }),\n  };\n};\n```\n\nThe advantage of having a reference to the effect (via the returned effect `entity`) is that you can explicitly [stop those effects](#effect-cleanup):\n\n```js\nconst someReducer = (state, event, exec) =\u003e {\n  // ...\n\n  // Stop an effect entity\n  exec.stop(state.someEffect);\n\n  return {\n    ...state,\n    // state.someEffect is no longer needed\n    someEffect: undefined,\n  };\n};\n```\n\n## Effect Cleanup\n\nInstead of implicitly relying on arbitrary values in a dependency array changing to stop an effect (as you would with `useEffect`), effects can be explicitly stopped using `exec.stop(entity)`, where `entity` is the effect entity returned from initially calling `exec(effect)`:\n\n```js\nconst timerReducer = (state, event, exec) =\u003e {\n  if (event.type === 'START') {\n    return {\n      ...state,\n      timer: exec(() =\u003e {\n        const id = setTimeout(() =\u003e {\n          // Do some delayed effect\n        }, 1000);\n\n        // Disposal function - will be called when\n        // effect entity is stopped\n        return () =\u003e {\n          clearTimeout(id);\n        };\n      }),\n    };\n  } else if (event.type === 'STOP') {\n    // Stop the effect entity\n    exec.stop(state.timer);\n\n    return state;\n  }\n\n  return state;\n};\n```\n\nAll running effect entities will automatically be stopped when the component unmounts.\n\n## Replacing Effects\n\nIf you want to replace an effect with another (likely similar) effect, instead of calling `exec.stop(entity)` and calling `exec(effect)` to manually replace an effect, you can call `exec.replace(entity, effect)` as a shorthand:\n\n```js\nconst doSomeDelay = () =\u003e {\n  const id = setTimeout(() =\u003e {\n    // do some delayed effect\n  }, delay);\n\n  return () =\u003e {\n    clearTimeout(id);\n  };\n};\n\nconst timerReducer = (state, event, exec) =\u003e {\n  if (event.type === 'START') {\n    return {\n      ...state,\n      timer: exec(() =\u003e doSomeDelay()),\n    };\n  } else if (event.type === 'LAP') {\n    // Replace the currently running effect represented by `state.timer`\n    // with a new effect\n    return {\n      ...state,\n      timer: exec.replace(state.timer, () =\u003e doSomeDelay()),\n    };\n  } else if (event.type === 'STOP') {\n    // Stop the effect entity\n    exec.stop(state.timer);\n\n    return state;\n  }\n\n  return state;\n};\n```\n\n## String Events\n\nThe events handled by the effect reducers are intended to be event objects with a `type` property; e.g., `{ type: 'FETCH', other: 'data' }`. For events without payload, you can dispatch the event type alone, which will be converted to an event object inside the effect reducer:\n\n```js\n// dispatched as `{ type: 'INC' }`\n// and is the same as `dispatch({ type: 'INC' })`\ndispatch('INC');\n```\n\n## API\n\n### `useEffectReducer` hook\n\nThe `useEffectReducer` hook takes the same first 2 arguments as the built-in `useReducer` hook, and returns the current `state` returned from the effect reducer, as well as a `dispatch` function for sending events to the reducer.\n\n```js\nconst SomeComponent = () =\u003e {\n  const [state, dispatch] = useEffectReducer(someEffectReducer, initialState);\n\n  // ...\n};\n```\n\nThe 2nd argument to `useEffectReducer(...)` can either be a static `initialState` or a function that takes in `exec` and returns an `initialState` (with executed initial effects). See [Initial Effects](#initial-effects) for more information.\n\n```js\nconst SomeComponent = () =\u003e {\n  const [state, dispatch] = useEffectReducer(\n    someEffectReducer,\n    exec =\u003e {\n      exec({ type: 'someEffect' });\n      return someInitialState;\n    },\n    {\n      someEffect(state, effect) {\n        // ...\n      },\n    }\n  );\n\n  // ...\n};\n```\n\nAdditionally, the `useEffectReducer` hook takes a 3rd argument, which is the implementation details for [named effects](#named-effects):\n\n```js\nconst SomeComponent = () =\u003e {\n  const [state, dispatch] = useEffectReducer(someEffectReducer, initialState, {\n    log: (state, effect, dispatch) =\u003e {\n      console.log(state);\n    },\n  });\n\n  // ...\n};\n```\n\n### `exec(effect)`\n\nUsed in an effect reducer, `exec(effect)` queues the `effect` for execution and returns an [effect entity](#effect-entities).\n\nThe `effect` can either be an effect object:\n\n```js\n// ...\nconst entity = exec({\n  type: 'alert',\n  message: 'hello',\n});\n```\n\nOr it can be an inline effect implementation:\n\n```js\n// ...\nconst entity = exec(() =\u003e {\n  alert('hello');\n});\n```\n\n### `exec.stop(entity)`\n\nUsed in an effect reducer, `exec.stop(entity)` stops the effect represented by the `entity`. Returns `void`.\n\n```js\n// Queues the effect entity for disposal\nexec.stop(someEntity);\n```\n\n## `exec.replace(entity, effect)`\n\nUsed in an effect reducer, `exec.replace(entity, effect)` does two things:\n\n1. Queues the `entity` for disposal (same as calling `exec.stop(entity)`)\n2. Returns a new [effect entity](#effect-entities) that represents the `effect` that replaces the previous `entity`.\n\n## TypeScript\n\nThe effect reducer can be specified as an `EffectReducer\u003cTState, TEvent, TEffect\u003e`, where the generic types are:\n\n- The `state` type returned from the reducer\n- The `event` object type that can be dispatched to the reducer\n- The `effect` object type that can be executed\n\n```ts\nimport { useEffectReducer, EffectReducer } from 'use-effect-reducer';\n\ninterface User {\n  name: string;\n}\n\ntype FetchState =\n  | {\n      status: 'idle';\n      user: undefined;\n    }\n  | {\n      status: 'fetching';\n      user: User | undefined;\n    }\n  | {\n      status: 'fulfilled';\n      user: User;\n    };\n\ntype FetchEvent =\n  | {\n      type: 'FETCH';\n      user: string;\n    }\n  | {\n      type: 'RESOLVE';\n      data: User;\n    };\n\ntype FetchEffect = {\n  type: 'fetchFromAPI';\n  user: string;\n};\n\nconst fetchEffectReducer: EffectReducer\u003cFetchState, FetchEvent, FetchEffect\u003e = (\n  state,\n  event,\n  exec\n) =\u003e {\n  switch (event.type) {\n    case 'FETCH':\n    // State, event, and effect types will be inferred!\n\n    // Also you should probably switch on\n    // `state.status` first ;-)\n\n    // ...\n\n    default:\n      return state;\n  }\n};\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdavidkpiano%2FuseEffectReducer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdavidkpiano%2FuseEffectReducer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdavidkpiano%2FuseEffectReducer/lists"}