{"id":25344742,"url":"https://github.com/iyegoroff/use-backlash","last_synced_at":"2026-05-19T03:36:29.534Z","repository":{"id":47573473,"uuid":"515379926","full_name":"iyegoroff/use-backlash","owner":"iyegoroff","description":"useReducer with effects","archived":false,"fork":false,"pushed_at":"2023-08-15T22:36:53.000Z","size":1074,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-02T23:06:05.257Z","etag":null,"topics":["effect","hook","react","reducer","useeffect","usereducer"],"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/iyegoroff.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,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-07-19T00:16:11.000Z","updated_at":"2023-07-30T22:34:33.000Z","dependencies_parsed_at":"2024-12-04T19:31:28.775Z","dependency_job_id":"6d5b4a67-3a2a-4555-981f-7f19f7608636","html_url":"https://github.com/iyegoroff/use-backlash","commit_stats":{"total_commits":82,"total_committers":1,"mean_commits":82.0,"dds":0.0,"last_synced_commit":"a1041dfce36ab6176c4880d0534f431fc4b5e46f"},"previous_names":[],"tags_count":30,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iyegoroff%2Fuse-backlash","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iyegoroff%2Fuse-backlash/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iyegoroff%2Fuse-backlash/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iyegoroff%2Fuse-backlash/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/iyegoroff","download_url":"https://codeload.github.com/iyegoroff/use-backlash/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247873637,"owners_count":21010504,"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":["effect","hook","react","reducer","useeffect","usereducer"],"created_at":"2025-02-14T11:50:59.340Z","updated_at":"2026-05-19T03:36:29.501Z","avatar_url":"https://github.com/iyegoroff.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# use-backlash\n\n[![npm](https://img.shields.io/npm/v/use-backlash)](https://npm.im/use-backlash)\n[![build](https://github.com/iyegoroff/use-backlash/workflows/build/badge.svg)](https://github.com/iyegoroff/use-backlash/actions/workflows/build.yml)\n[![publish](https://github.com/iyegoroff/use-backlash/workflows/publish/badge.svg)](https://github.com/iyegoroff/use-backlash/actions/workflows/publish.yml)\n[![codecov](https://codecov.io/gh/iyegoroff/use-backlash/branch/main/graph/badge.svg?token=YC314L3ZF7)](https://codecov.io/gh/iyegoroff/use-backlash)\n[![Type Coverage](https://img.shields.io/badge/dynamic/json.svg?label=type-coverage\u0026prefix=%E2%89%A5\u0026suffix=%\u0026query=$.typeCoverage.atLeast\u0026uri=https%3A%2F%2Fraw.githubusercontent.com%2Fiyegoroff%2Fuse-backlash%2Fmain%2Fpackage.json)](https://github.com/plantain-00/type-coverage)\n![Libraries.io dependency status for latest release](https://img.shields.io/librariesio/release/npm/use-backlash/0.0.32)\n[![bundlejs](https://deno.bundlejs.com/?q=use-backlash@0.0.32,use-backlash@0.0.32\u0026treeshake=[*],[{+default+}]\u0026badge=)](https://bundlejs.com/?q=use-backlash)\n[![npm](https://img.shields.io/npm/l/use-backlash.svg?t=1495378566926)](https://www.npmjs.com/package/use-backlash)\n\nuseReducer with effects\n\n## Getting started\n\n```\nnpm i use-backlash\n```\n\n## Description\n\nThis hook is a basic approach to split view/logic/effects in React. It works in [StrictMode](https://reactjs.org/docs/strict-mode.html) and is easy to [test](test/index.spec.tsx#L5-L27). It is designed to be framework-agnostic and was tested with [react](/test/react.spec.tsx) and [preact](/test/preact.spec.tsx).\n\n## Tutorial\n\nThis is going to be a Counter.\n\n```ts\nimport React, { useRef, useState, useEffect, useLayoutEffect } from 'react'\nimport { UpdateMap, createBacklash } from 'use-backlash'\n\n// A framework should provide react-like hooks\nconst useBacklash = createBacklash({ useRef, useState, useEffect, useLayoutEffect })\n\n// State can be anything,\ntype State = number\n\n// but an Action is always a record of tuples, where the key\n// is the name of an action and value is a list of arguments.\ntype Action = {\n  inc: []\n  dec: []\n}\n\n// init is a pure (in react terms) function that has no arguments\n// and just returns the initial state wrapped in array.\nconst init = () =\u003e [0] as const\n\n// Unlike the standard useReducer, update/reducer is not a function with\n// a switch statement inside, it is an object where each key is an action\n// name and each value is a reducer that takes a state, rest action\n// elements (if any) and returns next state wrapped in array. There is\n// a helper UpdateMap type, that checks the shape of update object and\n// makes writing types by hand optional.\nconst update: UpdateMap\u003cState, Action\u003e = {\n  inc: (state) =\u003e [state + 1],\n\n  dec: (state) =\u003e [state - 1]\n}\n\nexport const Counter = () =\u003e {\n  // In this example useBacklash hook takes init \u0026 update functions and\n  // returns a tuple containing state \u0026 actions. Note that 'init' \u0026 'update'\n  // arguments of useBacklash is 'initial' and changing these things won't\n  // affect the behavior of the hook. Also the actions object is guaranteed\n  // to remain the same during rerenders just like useReducer's dispatch\n  // function.\n  const [state, actions] = useBacklash(init, update)\n\n  return (\n    \u003c\u003e\n      \u003cdiv\u003e{state}\u003c/div\u003e\n      \u003cbutton onClick={actions.inc}\u003einc\u003c/button\u003e\n      \u003cbutton onClick={actions.dec}\u003edec\u003c/button\u003e\n    \u003c/\u003e\n  )\n}\n```\n\nPassing arguments to init function.\n\n```ts\n// Let's change the init function to have a single parameter\nconst init = (count: number) =\u003e [count] as const\n\n// ...\n\nexport const Counter = () =\u003e {\n  // Inside useBacklash body init function is called only once,\n  // so it is ok to inline it.\n  const [state, actions] = useBacklash(() =\u003e init(5), update)\n\n  // ...\n}\n```\n\nFor now `useBacklash` was used just as a fancy `useReducer` that returns an actions object instead of dispatch function. It doesn't make much sense to use it like this instead of `useReducer`. So let's make that counter persistent and see how `useBacklash` helps to handle side effects.\n\n```ts\n// We are going to use localStorage to store the state of the Counter.\n// Since I/O is a side effect it can not be called directly from the init\n// function. To model the situation 'state is not set yet' State type will\n// be extended with 'loading' string literal.\ntype State = 'loading' | number\n\n// Additional action 'loaded' will notify that Counter state is loaded.\ntype Action = {\n  loaded: [count: number]\n  inc: []\n  dec: []\n}\n\nconst key = 'counter_key'\n\n// init and each update property functions return\n// the value of Command type - [State, ...Effect[]]\nexport const init = (): Command\u003cState, Action\u003e =\u003e [\n  'loading',\n  // The next function is a side effect that will be called by useBacklash\n  // internally. Here it has single parameter - the same actions object\n  // that is returned from useBacklash call.\n  ({ loaded }) =\u003e loaded(Number(localStorage.getItem(key)) || 0)\n  // Additional can be added after the first one\n  // and all of them will run in order.\n]\n\nexport const update: UpdateMap\u003cState, Action\u003e = {\n  // The second parameter is a value that was passed to the 'loaded' action\n  // a few lines earlier.\n  loaded: (_, count) =\u003e [count],\n\n  inc: (state) =\u003e {\n    // If someone manages to call 'inc' before the state is loaded,\n    // just do nothing, that's the normal strategy for this example.\n    if (state === 'loading') {\n      return [state]\n    }\n\n    const next = state + 1\n\n    // Like the init function an update returns a Command\n    return [next, () =\u003e localStorage.setItem(key, `${next}`)]\n  },\n\n  dec: (state) =\u003e {\n    if (state === 'loading') {\n      return [state]\n    }\n\n    // This line is the only difference between 'inc' and 'dec'.\n    // Probably I will refactor it someday...\n    const next = state - 1\n\n    return [next, () =\u003e localStorage.setItem(key, `${next}`)]\n  }\n}\n\nexport const Counter = () =\u003e {\n  const [state, actions] = useBacklash(init, update)\n\n  return state === 'loading' ? null : (\n    \u003c\u003e\n      \u003cdiv\u003e{state}\u003c/div\u003e\n      \u003cbutton onClick={actions.inc}\u003einc\u003c/button\u003e\n      \u003cbutton onClick={actions.dec}\u003edec\u003c/button\u003e\n    \u003c/\u003e\n  )\n}\n```\n\nSample test.\n\n```ts\nimport { act, renderHook } from '@testing-library/react'\nimport { useBacklash } from '../src'\nimport { init, update } from '../src/Counter'\n\ndescribe.only('Counter', () =\u003e {\n  test('state should equal 1 after inc', () =\u003e {\n    const { result } = renderHook(() =\u003e useBacklash(init, update))\n\n    act(() =\u003e {\n      result.current[1].inc()\n    })\n\n    expect(result.current[0]).toEqual(1)\n  })\n})\n```\n\nWhen running this test with `jest` in `jsdom` test environment everything works as expected. But let's imagine that we don't have access to `localStorage` in our test environment. In this case test will fail with error: `ReferenceError: localStorage is not defined`. To avoid these kind of errors, `useBacklash` has an optional third parameter - `injects`. This parameter's value will be passed as a second argument to every effect function.\n\n```diff\n  import React from 'react'\n  import { Command, UpdateMap, useBacklash } from '../'\n\n  type State = 'loading' | number\n\n  type Action = {\n    loaded: [count: number]\n    inc: []\n    dec: []\n  }\n\n+ type Injects = {\n+   readonly getItem: Storage['getItem']\n+   readonly setItem: Storage['setItem']\n+ }\n\n  const key = 'counter_key'\n\n- export const init = (): Command\u003cState, Action\u003e =\u003e [\n+ export const init = (): Command\u003cState, Action, Injects\u003e =\u003e [\n    'loading',\n-   ({ loaded }) =\u003e loaded(Number(localStorage.getItem(key)) || 0)\n+   ({ loaded }, { getItem }) =\u003e loaded(Number(getItem(key)) || 0)\n  ]\n\n- export const update: UpdateMap\u003cState, Action\u003e = {\n+ export const update: UpdateMap\u003cState, Action, Injects\u003e = {\n    loaded: (_, count) =\u003e [count],\n\n    inc: (state) =\u003e {\n      if (state === 'loading') {\n        return [state]\n      }\n\n      const next = state + 1\n\n-     return [next, () =\u003e localStorage.setItem(key, `${next}`)]\n+     return [next, (_, { setItem }) =\u003e setItem(key, `${next}`)]\n    },\n\n    dec: (state) =\u003e {\n      if (state === 'loading') {\n        return [state]\n      }\n\n      const next = state - 1\n\n-     return [next, () =\u003e localStorage.setItem(key, `${next}`)]\n+     return [next, (_, { setItem }) =\u003e setItem(key, `${next}`)]\n    }\n  }\n\n  export const Counter = () =\u003e {\n-   const [state, actions] = useBacklash(init, update)\n+   // Updating 'injects' doesn't trigger rerenders, so it is safe to inline it.\n+   const [state, actions] = useBacklash(init, update, {\n+     getItem: ((...args) =\u003e localStorage.getItem(...args)) as Storage['getItem'],\n+     setItem: ((...args) =\u003e localStorage.setItem(...args)) as Storage['setItem']\n+   })\n\n    return state === 'loading' ? null : (\n      \u003c\u003e\n        \u003cdiv\u003e{state}\u003c/div\u003e\n        \u003cbutton onClick={actions.inc}\u003einc\u003c/button\u003e\n        \u003cbutton onClick={actions.dec}\u003edec\u003c/button\u003e\n      \u003c/\u003e\n    )\n}\n```\n\nNow the test can be rewritten with mocked `localStorage`:\n\n```ts\ntest('state should equal 1 after inc', () =\u003e {\n  let storage: string | null = null\n\n  const { result } = renderHook(() =\u003e\n    useBacklash(init, update, {\n      getItem: (_: string) =\u003e storage,\n      setItem: (_: string, value: string) =\u003e {\n        storage = `${value}`\n      }\n    })\n  )\n\n  act(() =\u003e {\n    result.current[1].inc()\n  })\n\n  expect(result.current[0]).toEqual(1)\n  expect(storage).toEqual('1')\n})\n```\n\n# Trivia\n\nIt was developed as a boilerplate-free substitute of [ts-elmish](https://github.com/iyegoroff/ts-elmish) project. While it doesn't support effect composition or complex effect creators, it should be easier to grasp and have enough power to handle important parts of the UI-logic for any component.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fiyegoroff%2Fuse-backlash","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fiyegoroff%2Fuse-backlash","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fiyegoroff%2Fuse-backlash/lists"}