{"id":23635332,"url":"https://github.com/filefoxper/airma","last_synced_at":"2025-08-31T11:30:53.951Z","repository":{"id":65526922,"uuid":"573420416","full_name":"filefoxper/airma","owner":"filefoxper","description":"It is a method calling useReducer. It is simple for usage with more easier typescript support.","archived":false,"fork":false,"pushed_at":"2025-08-29T07:45:56.000Z","size":3321,"stargazers_count":5,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-08-30T06:55:49.164Z","etag":null,"topics":["closure","hooks","method-dispatch","methods","react","react-context","redux","state","state-management","typescript","usereducer"],"latest_commit_sha":null,"homepage":"https://filefoxper.github.io/airma/","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/filefoxper.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","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,"zenodo":null}},"created_at":"2022-12-02T12:26:41.000Z","updated_at":"2025-08-20T11:27:31.000Z","dependencies_parsed_at":"2024-06-21T20:18:39.475Z","dependency_job_id":"08cf64c6-59c0-4493-9491-822ab819d90f","html_url":"https://github.com/filefoxper/airma","commit_stats":{"total_commits":269,"total_committers":2,"mean_commits":134.5,"dds":"0.0074349442379182396","last_synced_commit":"63d4f763798ed4a366253208f5b5c0292e4d0d15"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/filefoxper/airma","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/filefoxper%2Fairma","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/filefoxper%2Fairma/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/filefoxper%2Fairma/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/filefoxper%2Fairma/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/filefoxper","download_url":"https://codeload.github.com/filefoxper/airma/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/filefoxper%2Fairma/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":272973099,"owners_count":25024448,"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","status":"online","status_checked_at":"2025-08-31T02:00:09.071Z","response_time":79,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":["closure","hooks","method-dispatch","methods","react","react-context","redux","state","state-management","typescript","usereducer"],"created_at":"2024-12-28T05:33:59.853Z","updated_at":"2025-08-31T11:30:53.941Z","avatar_url":"https://github.com/filefoxper.png","language":"TypeScript","readme":"[![npm][npm-image]][npm-url]\n[![NPM downloads][npm-downloads-image]][npm-url]\n[![standard][standard-image]][standard-url]\n\n[npm-image]: https://img.shields.io/npm/v/%40airma/react-state.svg?style=flat-square\n[npm-url]: https://www.npmjs.com/package/%40airma/react-state\n[standard-image]: https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square\n[standard-url]: http://npm.im/standard\n[npm-downloads-image]: https://img.shields.io/npm/dm/%40airma/react-state.svg?style=flat-square\n\n# @airma\n\n`@airma` provides simple and useful packages for react developing env:\n\n1. [@airma/react-state](https://filefoxper.github.io/airma/#/react-state/index)\n2. [@airma/react-effect](https://filefoxper.github.io/airma/#/react-effect/index)\n3. [@airma/react-hooks](https://filefoxper.github.io/airma/#/react-hooks/index)\n\n## @airma/react-state\n\nSimple `reducer-like` state-management with method action dispatch mode for react components.\n\nCreate `reducer-like` function:\n\n```js\nexport function counting(state:number){\n    return {\n        // reproduced state for render\n        count: state,\n        // action method\n        increase:()=\u003estate + 1,\n        // action method\n        decrease:()=\u003estate - 1,\n        // action method, define parameters freely.\n        add(...additions: number[]){\n            return additions.reduce((result, current)=\u003e{\n                return result + current;\n            }, state);\n        }\n    };\n}\n```\n\nUse `reducer-like` function:\n\n```tsx\nimport {counting} from './model';\nimport {useModel} from '@airma/react-state';\n\n......\n// give it an initialState can make it fly.\nconst {count, increase, decrease, add} = useModel(counting, 0); // initialState `0`\n// call method `increase\\decrease\\add` can change `count` and make component rerender\n......\n```\n\nThe `reducer-like` function has a simple name `model`. Use API `model` can make it more simple.\n\n### Local state management\n\n```tsx\nimport {model} from '@airma/react-state';\n\n// api model returns a wrap function for your model function.\n// it keeps a same type of parameters and return data with the wrapped function.\nconst counting = model(function counting(state:number){\n    return {\n        count: state,\n        increase:()=\u003estate + 1,\n        decrease:()=\u003estate - 1,\n        add(...additions: number[]){\n            return additions.reduce((result, current)=\u003e{\n                return result + current;\n            }, state);\n        }\n    };\n});\n......\n// you can get useModel from the model wrapped function.\nconst {count, increase, decrease, add} = counting.useModel(0);\n......\n```\n\nThough, the basic function about `model` is enhancing `React.useReducer` to manage a local state, it also supports store usages. \n\n### Dynamic store usage\n\n```tsx\nimport {memo} from 'react';\nimport {model, provide} from '@airma/react-state';\n\nconst countingKey = model(function counting(state:number){\n    return {\n        count: state,\n        increase:()=\u003estate + 1,\n        decrease:()=\u003estate - 1,\n    };\n}).createKey(0);\n......\nconst Increase = memo(()=\u003e{\n    // use key.useSelector to sync state changes from store,\n    // when the selected result is changed it rerender component. \n    const increase = countingKey.useSelector(i =\u003e i.increase);\n    return \u003cbutton onClick={increase}\u003e+\u003c/button\u003e;\n});\nconst Count = memo(()=\u003e{\n    // use key.useModel to sync state changes from store.\n    const {count} = countingKey.useModel();\n    return \u003cspan\u003e{count}\u003c/span\u003e;\n});\nconst Decrease = memo(()=\u003e{\n    const decrease = countingKey.useSelector(i =\u003e i.decrease);\n    return \u003cbutton onClick={decrease}\u003e-\u003c/button\u003e;\n});\n// provide key to component for creating a dynamic store in Component\nconst Component = provide(countingKey).to(function Comp() {\n    return (\n        \u003cdiv\u003e\n            \u003cIncrease/\u003e\n            \u003cCount/\u003e\n            \u003cDecrease/\u003e\n        \u003c/div\u003e\n    );\n});\n......\n```\n\nThe keys are templates for creating store in component. When the component generates an element, it creates a store in this element, and provide the store to its children. This store is a dynamic store. \n\nThe dynamic store is created when component creates elements, that makes the elements from one component takes different stores with same temlates.\n\nThe static store is simple, it needs no Context tech support. Using `model(xxx).createStore()` can build a static store.\n\n### Static store\n\n```ts\nimport {model} from '@airma/react-state';\n\nconst countingStore = model(function counting(state:number){\n    return {\n        count: state,\n        increase:()=\u003estate + 1,\n        decrease:()=\u003estate - 1,\n        add(...additions: number[]){\n            return additions.reduce((result, current)=\u003e{\n                return result + current;\n            }, state);\n        }\n    };\n}).createStore(0);\n......\nconst Increase = memo(()=\u003e{\n    const increase = countingStore.useSelector(i =\u003e i.increase);\n    return \u003cbutton onClick={increase}\u003e+\u003c/button\u003e;\n});\nconst Count = memo(()=\u003e{\n    const {count} = countingStore.useModel();\n    return \u003cspan\u003e{count}\u003c/span\u003e;\n});\nconst Decrease = memo(()=\u003e{\n    const decrease = countingStore.useSelector(i =\u003e i.decrease);\n    return \u003cbutton onClick={decrease}\u003e-\u003c/button\u003e;\n});\n// A static store needs no provider.\nconst Component = function Comp() {\n    return (\n        \u003cdiv\u003e\n            \u003cIncrease/\u003e\n            \u003cCount/\u003e\n            \u003cDecrease/\u003e\n        \u003c/div\u003e\n    );\n};\n```\n\nThe `useSelector` API is helpful for reducing render frequency, only when the selected result is changed, it make its owner component rerender. \n\nThere are more examples, concepts and APIs in the [documents](https://filefoxper.github.io/airma/#/react-state/index) of `@airma/react-state`.\n\n## @airma/react-effect\n\nSimple asynchronous state-management for react. It is considered as a composite hook by `React.useEffect` and `React.useState`.\n\nCreate a callback which always returns a promise.\n\n```ts\n// parameters groupId\nexport function fetchUsers(groupId: number): Promise\u003cUser[]\u003e {\n    return userRequest.fetchUsersByGroupId(groupId);\n}\n```\n\nUse asynchronous callback.\n\n```tsx\nimport {fetchUsers} from './session';\nimport {useQuery} from '@airma/react-effect';\n\n......\n// useQuery calls `fetchUsers` just like a `useEffect` works.\n// When the owner component is mounting, or each variable([props.groupId]) is changing, the `fetchUsers` is called. \nconst [\n    sessionState,\n    recall,\n    recallWithVariables\n] = useQuery(fetchUsers, [props.groupId]);\n\nconst {\n    // (Users[] | undefined), the promise resolving data.\n    // Before useQuery works out, it is undefined.\n    data: users,\n    // boolean, if useQuery is fetching data.\n    isFetching,\n    // boolean, if current query has a rejection.\n    isError,\n    // unknown, the promise rejection\n    error,\n    // (undefined | Parameters\u003ctypeof fetchUsers\u003e),\n    // The parameters of current query result.\n    // Before useQuery works out, it is undefined.\n    variables,\n    // boolean, if useQuery has fetched data successfully.\n    loaded\n} = sessionState;\n......\n// call `recall` function can trigger useQuery works manually.\nrecall();\n......\n// call `recallWithVariables` function can trigger useQuery works manually with temporary parameters.\nrecallWithVariables(props.groupId);\n......\n```\n\nEvery time `useQuery` fetches a latest data as `sessionState.data` by calling asynchronous callback, it is very useful.\n\nThe asynchronous callback for `useQuery` or `useMutation` is named `session` in `@airma/react-effect`. It makes a simple usage `API` like `model` for `@airma/react-state`.\n\n### Local asynchronous state management\n\n```ts\nimport {session} from '@airma/react-effect';\n\nconst fetchUsersSession = session((groupId: number): Promise\u003cUser[]\u003e =\u003e {\n    return userRequest.fetchUsersByGroupId(groupId);\n}, 'query'); // use sessionType `query` to mark out, it is a session for `useQuery` not `useMutation`.\n......\nconst [\n    sessionState,\n    recall,\n    recallWithVariables \n] = fetchUsersSession.useQuery([props.groupId]);\n......\n```\n\nAPI `useQuery` or `useMutation` can be used as a context or global asynchronous state-management hook too.\n\n### Dynamic asynchronous state management\n\n```tsx\nimport {session, provide} from '@airma/react-effect';\n\n// create a key for syncing state changes.\nconst fetchUsersSessionKey = session((groupId: number): Promise\u003cUser[]\u003e =\u003e {\n    return userRequest.fetchUsersByGroupId(groupId);\n}, 'query').createKey();\n......\nconst ChildQueryComponent = ()=\u003e{\n    ......\n    // when `fetchUsersSessionKey.useQuery` works,\n    // the `sessionState change` happens in a store,\n    // the other session usages like `useSession` with same key receives same change.\n    const [\n        sessionState,\n        recall,\n        recallWithVariables \n    ] = fetchUsersSessionKey.useQuery([props.groupId]);\n    ......\n};\n\nconst ChildReceptionComponent = ()=\u003e{\n    ......\n    // the key.useSession can accept the sessionState changes caused by the same store `useQuery` or `useMutation`.\n    const [\n        sessionState,\n        recall\n    ] = fetchUsersSessionKey.useSession();\n    ......\n};\n\n// provide keys to component for creating a dynamic store.\nconst Component = provide(fetchUsersSessionKey).to(function Comp(){\n    return (\n        \u003c\u003e\n            \u003cChildQueryComponent/\u003e\n            \u003cChildReceptionComponent/\u003e\n        \u003c/\u003e\n    );\n});\n```\n\nUse `session(xxx,'query'|'mutation').createStore()` can create a static store, which can be used without provider.\n\n### Static asynchronous state management\n\n```tsx\nimport {session} from '@airma/react-effect';\n\n// create static store\nconst fetchUsersSession = session((groupId: number): Promise\u003cUser[]\u003e =\u003e {\n    return userRequest.fetchUsersByGroupId(groupId);\n}, 'query').createStore();\n......\nconst ChildQueryComponent = ()=\u003e{\n    ......\n    const [\n        sessionState,\n        recall,\n        recallWithVariables \n    ] = fetchUsersSession.useQuery([props.groupId]);\n    ......\n};\n\nconst ChildReceptionComponent = ()=\u003e{\n    ......\n    const [\n        sessionState,\n        recall\n    ] = fetchUsersSession.useSession();\n    ......\n};\n \n // The static store API can be used directly without provider.\nconst Component = function Comp(){\n    return (\n        \u003c\u003e\n            \u003cChildQueryComponent/\u003e\n            \u003cChildReceptionComponent/\u003e\n        \u003c/\u003e\n    );\n};\n```\n\n### API useMutation\n\nThe API `useMutation` or `(session\\sessionStore).useMutation` is similar with `useQuery`.\n\nThe different is `useMutation` should be triggered to work manually.\n\n```ts\nimport {session} from '@airma/react-effect';\n\nconst saveUserSession = session((user: User): Promise\u003cvoid\u003e =\u003e {\n    return userRequest.saveUser(user);\n}, 'mutation'); // use sessionType `mutation` to mark out, it is a session for `useMutation` not `useQuery`.\n......\nconst [\n    sessionState,\n    recall,\n    recallWithVariables \n] = saveUserSession.useMutation([state.user]);\n......\n// trigger it manually\nrecall();\n......\nrecallWithVariables();\n```\n\nThe `useMutation` API works with a block mode. If it is working, it refuses other triggered missions.\n\n### Change default trigger ways\n\nSet `triggerOn` config property to `useQuery` or `useMutation` can change the default trigger ways of these hooks.\n\nMake `useMutation` works when variables changes.\n\n```ts\nimport {session} from '@airma/react-effect';\n\nconst saveUserSession = session((user: User): Promise\u003cvoid\u003e =\u003e {\n    return userRequest.saveUser(user);\n}, 'mutation'); \n......\nconst [\n    sessionState,\n    recall,\n    recallWithVariables \n] = saveUserSession.useMutation({\n    variables: [state.user],\n    // This setting makes useMutation works when `state.user` changes.\n    triggerOn: ['update', 'manual']\n});\n......\nrecall();\n......\nrecallWithVariables();\n```\n\nBe careful if the config of `useMutation` is using `update` or `mount` trigger way, it works without a block  `protection` mode.\n\nMake `useQuery` works only when it is triggered manually.\n\n```ts\nimport {session} from '@airma/react-effect';\n\nconst fetchUsersSession = session((groupId: number): Promise\u003cUser[]\u003e =\u003e {\n    return userRequest.fetchUsersByGroupId(groupId);\n}, 'query'); \n......\nconst [\n    sessionState,\n    recall,\n    recallWithVariables \n] = fetchUsersSession.useQuery({\n    variables: [props.groupId],\n    triggerOn: ['manual']\n});\n......\n```\n\n### Strategies\n\nThere are full strategies to decorate the actions about `useQuery` and `useMutation`, like debounce, throttle, once, memo, and so on.\n\nSet these strategies to `useQuery` or `useMutation` can help session works better.\n\n```ts\nimport {session} from '@airma/react-effect';\n\nconst fetchUsersSession = session((groupId: number): Promise\u003cUser[]\u003e =\u003e {\n    return userRequest.fetchUsersByGroupId(groupId);\n}, 'query'); \n......\nconst [\n    sessionState,\n    recall,\n    recallWithVariables \n] = fetchUsersSession.useQuery({\n    variables: [props.groupId],\n    // Strategy.debounce makes useQuery works debounce with 300 ms duration time.\n    // Strategy.memo makes useQuery use the old sessionState.data, if the work out data equals old data by calling `JSON.stringify`.\n    strategy: [Strategy.debounce(300), Strategy.memo()]\n});\n......\n```\n\n### Questions?\n\n`@airma/react-effect` dependent `@airma/react-state`, and the state sharing way is just like `@airma/react-state`.\n\nThere are more examples, concepts and APIs in the [documents](https://filefoxper.github.io/airma/#/react-effect/index) of `@airma/react-effect`.\n\n## @airma/react-hooks\n\nA lot of APIs about `@airma/react-state` and `@airma/react-effect` are too similar. So, `@airma/react-hooks` is a better choosen for using both of them. It combine these two packages APIs together.\n\n```tsx\nimport {model, session} from '@airma/react-hooks';\n\nconst countingStore = model(function counting(state:number){\n    return {\n        count: state,\n        increase:()=\u003estate + 1,\n        decrease:()=\u003estate - 1,\n        add(...additions: number[]){\n            return additions.reduce((result, current)=\u003e{\n                return result + current;\n            }, state);\n        }\n    };\n}).createStore(0);\n\nconst fetchUsersSession = session((groupId: number): Promise\u003cUser[]\u003e =\u003e {\n    return userRequest.fetchUsersByGroupId(groupId);\n}, 'query').createStore();\n......\n// combine different stores together, and provide to a root component\nconst Component = countingStore.with(fetchUsersSession).provideTo(function Comp(){\n    ......\n});\n```\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffilefoxper%2Fairma","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffilefoxper%2Fairma","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffilefoxper%2Fairma/lists"}