{"id":29926643,"url":"https://github.com/ecomfe/react-kiss","last_synced_at":"2025-08-02T12:43:00.012Z","repository":{"id":32825540,"uuid":"143244660","full_name":"ecomfe/react-kiss","owner":"ecomfe","description":"A simple and stupid react container solution","archived":false,"fork":false,"pushed_at":"2022-12-08T09:08:01.000Z","size":1436,"stargazers_count":84,"open_issues_count":25,"forks_count":5,"subscribers_count":12,"default_branch":"master","last_synced_at":"2025-06-30T01:05:30.744Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"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/ecomfe.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-08-02T05:01:57.000Z","updated_at":"2024-06-24T06:45:36.000Z","dependencies_parsed_at":"2023-01-14T22:21:24.809Z","dependency_job_id":null,"html_url":"https://github.com/ecomfe/react-kiss","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/ecomfe/react-kiss","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ecomfe%2Freact-kiss","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ecomfe%2Freact-kiss/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ecomfe%2Freact-kiss/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ecomfe%2Freact-kiss/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ecomfe","download_url":"https://codeload.github.com/ecomfe/react-kiss/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ecomfe%2Freact-kiss/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":268392180,"owners_count":24243297,"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-02T02:00:12.353Z","response_time":74,"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":[],"created_at":"2025-08-02T12:42:24.191Z","updated_at":"2025-08-02T12:42:59.992Z","avatar_url":"https://github.com/ecomfe.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# react-kiss\n\n[Redux](https://redux.js.org/) and [mobx-state-tree](https://github.com/mobxjs/mobx-state-tree) are both good state container solutions, however they introduces too many terminologies and bioperlates to structure a simple app.\n\nWe want something super simple and stupid, something that can define a state container everywhere at any granularity and combine them together to form a complete app. react-kiss is thus a state container which:\n\n- introduces minimal efforts to create and use states.\n- ships with both state and state transfer definitions, handles sync and async workflows naturally.\n- allows split states to different parts as small as possible and combine then together when required.\n- encourages establishing and joining state container in a smaller scope instead of a monotonous global state.\n\n## Install\n\n```shell\nnpm install react-kiss\n```\n\n## Region\n\nA region is a container of a state and some workflows to manipulate state in the context of a given payload.\n\n### State\n\nA state is a predefined structure and its current data, any plain object can be a state.\n\n### Workflow\n\nA workflow is a process which receives a payload and manipulates current state, a workflow can manipulates state either synchronously or asychronously, it is also possible manipulates state multiple times within a workflow.\n\nThere are 2 forms of workflows.\n\n#### Simple workflow\n\nA simple workflow is a simple function that receives a payload and current state, it should return either a state patch like:\n\n```javascript\nconst setCurrentUser = (user, state) =\u003e {\n    if (state.currentUser) {\n        return {};\n    }\n\n    return {\n        currentUser: user\n    };\n};\n```\n\nor a state updater function like:\n\n```javascript\nconst addValue = (amount, state) =\u003e {\n    if (state.value \u003e= 100) {\n        return {};\n    }\n\n    return ({value}) =\u003e ({value: value + amount});\n};\n```\n\n#### Composite workflow\n\nA composite workflow is a workflow which may manipulates state multiple times or involves async process, it is defined as a generator function:\n\n```javascript\nfunction* saveTodo(todo) {\n    yield {submitting: true};\n    try {\n        const newTodo = yield postTodo(todo);\n        yield state =\u003e {\n            const {todos} = state;\n\n            return {\n                todos: [...todos, newTodo],\n                submitting: false\n            };\n        };\n    }\n    catch (ex) {\n        yield {submitting: false, error: ex};\n    }\n};\n```\n\nThis generator function receives `(payload, getState)` as its arguments, and yields value in 3 types:\n\n- a simple object is treated as a state patch.\n- a function is treated as a state updater.\n- a `Promise` instance is treated as an async process, its resolved value or rejected error will returned back to `yield` expression.\n\n### Selector\n\nA selector is a pure function which computes and selects certain values from current state, selectors are defined as an object with function values:\n\n```javascript\n\nconst selectors = {\n    filterVisibleTodos({todos, filter}) {\n        return filter ? todos.filter(todo =\u003e todo.includes(filter)) : todos;\n    }\n};\n```\n\nSelectors can also receive arbitary arguments, the first argument is always the `currentState`, rest arguments are those passed to selector on invocation.\n\nWhen invoke a selector, the `currentState` argument is omitted (it is bound automatically), so the above selector is called just as `const todos = filterVisibleTodos()`;\n\n## Define a region\n\nTo define a region, we just need to provide an `initialState` and a map of `workflows` to `defineRegion` exported function:\n\n```javascript\nimport {defineRegion} from 'react-kiss';\n\nconst initialState = {\n    todos: [\n        'Buy milk',\n        'Meet John at peace park'\n    ],\n    filter: '',\n    error: null,\n    submitting: false\n};\n\nconst workflows = {\n    * saveTodo(todo) {\n        yield {submitting: true};\n        try {\n            const newTodo = yield postTodo(todo);\n            yield state =\u003e {\n                const {todos} = state;\n\n                return {\n                    todos: [...todos, newTodo],\n                    submitting: false\n                };\n            };\n        }\n        catch (ex) {\n            yield {submitting: false, error: ex};\n        }\n    },\n\n    filterByKeyword(keyword) {\n        return {filter: keyword};\n    }\n};\n\nconst selectors = {\n    filterVisibleTodos({todos, filter}) {\n        return filter ? todos.filter(todo =\u003e todo.includes(filter)) : todos;\n    }\n};\n\nconst todoRegion = defineRegion(initialState, workflows, selectors);\n\nexport const establishTodo = todoRegion.establish;\nexport const joinTodo = todoRegion.join;\n```\n\nThe return value of `defineRegion` function is an object containing `establish` and `join` function.\n\n## Establish a region\n\nBy `defineRegion` we get a region definition but it is not yet usable as a state container, we should establish it at a parent scope and join it from it's children.\n\nTo establish a region, call `establish` function returned by `defineRegion` like an HOC:\n\n```jsx\nimport {establishTodo} from 'regions';\n\nconst Todo = () =\u003e (\n    \u003cdiv\u003e\n        \u003cFilter /\u003e\n        \u003cList /\u003e\n        \u003cAddTodo /\u003e\n    \u003c/div\u003e\n);\n\nexport default establishTodo('Todo')(Todo);\n```\n\nThe only argument of `establish` function is an optional name of region, by enhancing a component with `establish`, it now acts as a context's `Provider` to manage the state.\n\nNote a region can be established in different places, just like using `Provider` in different places, a child receives state from the closest regions of same type.\n\n## Join a region\n\nAll children components under a component enhanced with `establish` can choose to join this region by invoking `join` function returned from `defineRegion`, in case a component is joined to a region, it automatically receives state and workflows from region, a `mapToProps` function is used to select props:\n\n```jsx\nimport {PureComponent} from 'react';\nimport {bind} from 'lodash-decorators';\nimport {joinTodo} from 'regions';\n\nclass AddTodo extends PureComponent {\n\n    state = {\n        todoText: ''\n    };\n\n    @bind()\n    syncTodoText(e) {\n        this.setState({todoText: e.target.value});\n    }\n\n    @bind()\n    async saveTodo() {\n        const {todoText} = this.state;\n        const {onSaveTodo} = this.props;\n\n        await onSaveTodo(todoText);\n        this.setState({todoText: ''});\n    }\n\n    componentDidUpdate(prevProps) {\n        if (this.props.error !== prevProps.error) {\n            alert(this.props.error.message); // eslint-disable-line no-alert\n        }\n    }\n\n    render() {\n        const {todoText} = this.state;\n        const {submitting} = this.props;\n\n        return (\n            \u003cdiv\u003e\n                \u003cinput value={todoText} onChange={this.syncTodoText} /\u003e\n                \u003cbutton type=\"button\" onClick={this.saveTodo} disabled={submitting}\u003e\n                    {submitting ? 'Submitting...' : 'Add Todo'}\n                \u003c/button\u003e\n            \u003c/div\u003e\n        );\n    }\n}\n\nconst mapToProps = ({submitting, error, saveTodo}) =\u003e ({submitting, error, onSaveTodo: saveTodo});\n\nexport default joinTodo(mapToProps)(AddTodo);\n```\n\nThis is very similar to react-redux's `connect` function except it only requires one `mapToProps` function.\n\n## Combine regions\n\nWe can establish region at any place, it is also straightforward to establish multiple regions with different types:\n\n```javascript\nimport {compose} from 'recompose';\nimport {establishTodo, establishNote} from 'regions';\n\nconst App = () =\u003e (\n    // ...\n);\n\nconst enhance = compose(\n    establishTodo('MyTodo'),\n    establishNote('Note')\n);\n\nexport default ehnahce(App);\n```\n\nNote that it is **NOT** OK to establish multiple regions with the same type (returned from the same `defineRegion` call), in such case only the latest region takes effects.\n\nWe can also join multiple regions using the `joinAll` exported function:\n\n```jsx\nimport {compose} from 'recompose';\nimport {joinAll} from 'react-kiss';\nimport {establishNote, joinNote, joinGlobal} from 'regions';\n\nconst Note = ({username, visible, message, onToggle}) =\u003e (\n    \u003cdiv style={{marginTop: 20}}\u003e\n        \u003cbutton type=\"button\" onClick={onToggle}\u003e\n            {visible ? 'Hide' : 'Show'}\n        \u003c/button\u003e\n        {visible \u0026\u0026 \u003cp style={{fontSize: 48, fontWeight: 'bold', textAlign: 'center'}}\u003e{message} @ {username}\u003c/p\u003e}\n    \u003c/div\u003e\n);\n\nconst mapToProps = (note, global) =\u003e {\n    const message = note.notes[global.username];\n\n    return {\n        username: global.username,\n        message: message,\n        visible: note.visible,\n        onToggle: note.toggle\n    };\n};\n\nconst enhance = compose(\n    establishNote('Note'),\n    joinAll(joinNote, joinGlobal, mapToProps)\n);\n\nexport default enhance(Note);\n```\n\nThe `joinAll` function receives multiple `join` functions and a `mapToProps` function, the `mapToProps` function receives all region contexts in the order `join` functions are given.\n\n## Transient region\n\nIn some cases we don't need a react's context to hold our state and workflows, the `withTransientRegion` HOC defines a region only for given component, it is a useful utility to separate state management from presetation.\n\n```jsx\nimport {withTransientRegion} from 'react-kiss';\n\nconst initialState = {\n    value: 0\n};\n\nconst workflows = {\n    increment(payload, {value}) {\n        return {value: value + 1};\n    },\n\n    decrement(payload, {value}) {\n        return {value: value - 1};\n    }\n};\n\n// The Counter component now is a pure presentational function component, state and workflows are defined in region\nconst Counter = ({value, increment, decrement}) =\u003e (\n    \u003cdiv\u003e\n        \u003cbutton type=\"button\" onClick={decrement}\u003edec\u003c/button\u003e\n        \u003cspan\u003e{value}\u003c/span\u003e\n        \u003cbutton type=\"button\" onClick={increment}\u003einc\u003c/button\u003e\n    \u003c/div\u003e\n);\n\nexport default withTransientRegion(initialState, workflows)(Counter);\n```\n\n## Specific regions\n\n`react-kiss` also provides some predefined regions to handle common scenarios.\n\n### Query\n\nThe `defineQueryRegion` function accepts a request function and defines a region in such structure:\n\n```javascript\n{\n    queries: {\n        [stringifiedParams]: {\n            pendingMutex: 0, // The number of on-the-way request\n            params: {}, // Requesting params\n            response: {\n                data: {}, // Response of success request\n                error: {} // Response of fail request\n            }\n        },\n        ...\n    },\n    request: function, // The workflow to trigger request\n    findQuery: function(params), // Selector to find query object by params\n    findReponse: function(params), // Selector to find query.response object by params\n    findData: function(params) // Selector to find query.response.data object by params\n}\n```\n\nFor each invocation of `request` workflow, a `[stringifiedParams]: Query` key-value pair is stored in `queries` state.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fecomfe%2Freact-kiss","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fecomfe%2Freact-kiss","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fecomfe%2Freact-kiss/lists"}