{"id":15434023,"url":"https://github.com/sebinsua/conventional-component","last_synced_at":"2025-04-19T18:08:41.021Z","repository":{"id":57206753,"uuid":"106751777","full_name":"sebinsua/conventional-component","owner":"sebinsua","description":"🏴 Hoist the state of React components into Redux.","archived":false,"fork":false,"pushed_at":"2018-02-21T18:34:05.000Z","size":281,"stargazers_count":6,"open_issues_count":1,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-01-07T09:04:40.415Z","etag":null,"topics":["best-practices","conventions","ducks","react","react-components","redux","reusability","standards"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sebinsua.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-10-12T22:28:51.000Z","updated_at":"2024-05-01T00:45:53.000Z","dependencies_parsed_at":"2022-09-08T17:01:11.439Z","dependency_job_id":null,"html_url":"https://github.com/sebinsua/conventional-component","commit_stats":null,"previous_names":[],"tags_count":15,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sebinsua%2Fconventional-component","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sebinsua%2Fconventional-component/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sebinsua%2Fconventional-component/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sebinsua%2Fconventional-component/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sebinsua","download_url":"https://codeload.github.com/sebinsua/conventional-component/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249758508,"owners_count":21321537,"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":["best-practices","conventions","ducks","react","react-components","redux","reusability","standards"],"created_at":"2024-10-01T18:36:45.418Z","updated_at":"2025-04-19T18:08:40.995Z","avatar_url":"https://github.com/sebinsua.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# `conventional-component` [![Build Status](https://travis-ci.org/sebinsua/conventional-component.png)](https://travis-ci.org/sebinsua/conventional-component) [![npm version](https://badge.fury.io/js/conventional-component.svg)](https://www.npmjs.com/package/conventional-component)\n\u003e 🏴 React components which can have their state hoisted into Redux.\n\nAs I search for components with which to build an application, I frequently find otherwise excellent components which have inaccessible state. Often this means complications when integrating with Redux, due to how the state is passed in and emitted. Sometimes I will try to find a `react-redux-` variant of a component, however these unfortunately lose the ease of integration of plain React components.\n\nThis is a proposal to build components out of reducers and actions and a library to help do so. The intention is to make it easy to write standardised components which (1) can be quickly installed into an app, and (2) can have their state hoisted into another state management library (Redux, MobX, etc) if this is required.\n\nIt's loosely inspired from the conventions within [`erikras/ducks-modular-redux`](https://github.com/erikras/ducks-modular-redux). It also has some similarities to [`multireducer`](https://github.com/erikras/multireducer) however due to its use of convention it's decoupled from redux.\n\n#### :warning: :construction_worker: :wrench: Ready-for-use yet WIP :hammer: :construction: :warning:\n\n- [ ] **Chore**: Finish unit tests. *(NOTE: It's already tested via an example within [`example/src`](./example/src)).*\n\n## Convention\n\n```js\nexport {\n  actions,\n  reducer,\n  withLogic,\n  Template,\n  Component,\n  REDUCER_NAME,\n  COMPONENT_NAME,\n  COMPONENT_KEY\n}\nexport default Component\n```\n\n### Rules\n\nA `Component`...\n\n1. **MUST** `export default` and `export` itself.\n    1. **MAY** `export` the name of the component as `COMPONENT_NAME`.\n    2. **MAY** `export` the primary key of each of the components as `COMPONENT_KEY` (e.g. `id`, `name`).\n2. **MUST** store its state using a reducer and some actions.\n    1. **MAY** use the higher-order component (HOC) `connectToState(reducer, actions)` to achieve this.\n3. **MUST** `export` its component logic as a higher-order component (HOC) `withLogic(Template)`.\n    1. **MUST** dispatch actions during the lifecycle of the component to describe its state.\n        1. **MUST** dispatch an `init(identity, props)` action on either construction or `componentWillMount`.\n        2. **MAY** dispatch a `receiveNextProps(identity, props)` action on `componentWillReceiveProps`.\n        3. **MUST** dispatch a `destroy(identity)` action on `componentWillUnmount`.\n        4. **MAY** use the higher-order component (HOC) `withLifecycleStateLogic` to achieve this.\n    2. **MAY** implement `withRenderProp` in order to render a user-specified render prop but otherwise fallback to rendering the `Template`.\n4. **MUST** `export` its action creator functions as `actions`.\n    1. **MUST** either wrap each of its actions with `withActionIdentity(actionCreator)` or use action creators with the same signature.\n5. **MUST** `export` its reducer as `reducer(state, action)`.\n    1. **MAY** `export` the default name for its reducer as `REDUCER_NAME`.\n6. **MUST** `export` its component template as `Template`.\n\n## Example\n\n### Component\n\nThe best way to understand the convention is to read [some example code for an `Input` component](./example/src/Input).\n\n#### Index\n\n```js\nimport Input, { COMPONENT_NAME, COMPONENT_KEY } from './Input'\nimport Template from './InputDisplay'\nimport withLogic from './withLogic'\nimport reducer, { REDUCER_NAME } from './reducer'\nimport * as actions from './actions'\n\nexport {\n  actions,\n  reducer,\n  withLogic,\n  Template,\n  REDUCER_NAME,\n  COMPONENT_NAME,\n  COMPONENT_KEY\n}\nexport default Input\n```\n\n#### `withLogic(Template)`\n\n```js\nimport React, { Component } from 'react'\nimport {\n  withRenderProp,\n  withLifecycleStateLogic\n} from 'conventional-component'\n\nimport InputDisplay from './InputDisplay'\n\nfunction withLogic(Template = InputDisplay) {\n  class Input extends Component {\n    onBlur = event =\u003e {\n      event.preventDefault()\n      return this.props.setFocus(false)\n    }\n\n    onChange = event =\u003e {\n      event.preventDefault()\n      return this.props.setValue(event.target.value)\n    }\n\n    onFocus = event =\u003e {\n      event.preventDefault()\n      return this.props.setFocus(true)\n    }\n\n    reset = event =\u003e {\n      event.preventDefault()\n      return this.props.setValue('')\n    }\n\n    render() {\n      const templateProps = {\n        ...this.props,\n        onBlur: this.onBlur,\n        onChange: this.onChange,\n        onFocus: this.onFocus,\n        reset: this.reset\n      }\n\n      if (\n        typeof this.props.render === 'function' ||\n        typeof this.props.children === 'function'\n      ) {\n        return withRenderProp(templateProps)\n      }\n\n      if (Template) {\n        return \u003cTemplate {...templateProps} /\u003e\n      }\n\n      return null\n    }\n  }\n\n  return withLifecycleStateLogic({\n    shouldDispatchReceiveNextProps: false\n  })(Input)\n}\n\nexport default withLogic\n```\n\n#### `Input`\n\n```js\nimport { compose } from 'recompose'\nimport { connectToState } from 'conventional-component'\n\nimport InputDisplay from './InputDisplay'\nimport withLogic from './withLogic'\nimport reducer from './reducer'\nimport * as actions from './actions'\n\nconst COMPONENT_NAME = 'Input'\nconst COMPONENT_KEY = 'name'\n\nconst enhance = compose(connectToState(reducer, actions), withLogic)\n\nconst Input = enhance(InputDisplay)\n\nexport { COMPONENT_NAME, COMPONENT_KEY }\nexport default Input\n```\n\n### Redux\n\nThe best way to understand how the state can be hoisted into Redux is to read [some example code in which this is done](./example/src/Redux).\n\n#### Component\n\n```js\nimport { asConnectedComponent } from 'conventional-component'\n\nimport {\n  actions,\n  withLogic,\n  Template,\n  REDUCER_NAME,\n  COMPONENT_NAME,\n  COMPONENT_KEY\n} from '../../Input'\n\nexport default asConnectedComponent({\n  actions,\n  withLogic,\n  Template,\n  REDUCER_NAME,\n  COMPONENT_NAME,\n  COMPONENT_KEY\n})\n```\n\n#### Reducer\n\n```js\nimport { withReducerIdentity } from 'conventional-component'\n\nimport { reducer, COMPONENT_NAME, REDUCER_NAME } from '../../Input'\n\nexport { REDUCER_NAME }\nexport default withReducerIdentity(COMPONENT_NAME, reducer)\n```\n\n## Install\n\n```sh\nyarn add conventional-component\n```\n\n## API\n\n### Component\n\n#### `connectToState(reducer, actionCreators) =\u003e Component =\u003e ConnectedComponent`\n\nThis function allows a `reducer` to be used in place of [standard `this.setState`](https://reactjs.org/docs/react-component.html#setstate) calls. It passes through the reducer state and the actions into a component.\n\nIt's implemented as a higher-order component (HOC) and therefore returns a function which takes a `Component`. In fact it might look familiar as it is an analogue to [`react-redux#connect(mapStateToProps, actions)`](https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options), however with first argument being a standard redux [`reducer`](http://redux.js.org/docs/basics/Reducers.html) and the second argument being an object of identity-receiving action creators (these could possibly have been created by wrapping normal action creators with `withActionIdentity`).\n\n#### `withLifecycleStateLogic({ shouldDispatchReceiveNextProps }) =\u003e LogicComponent =\u003e LifecycleLogicComponent`\n\nThis higher-order component (HOC) is provided to help dispatch the correct lifecycle\nactions (e.g. `init` and `destroy` when a component is added or removed from the screen.)\n\nIt should be used within `withLogic` to wrap any other component logic.\n\nBy default `shouldDispatchReceiveNextProps` is false.\n\n###### `init(identity, props)`\n\nThis must be called by conventional components during either [the `constructor` or the `componentWillMount` lifecycle methods](https://reactjs.org/docs/react-component.html#constructor).\n\n`INIT` is also exported alongside this.\n\n###### `receiveNextProps(identity, props)`\n\nThis may be called by conventional components during [the `componentWillReceiveProps` lifecycle method](https://reactjs.org/docs/react-component.html#componentwillreceiveprops).\n\n`RECEIVE_NEXT_PROPS` is also exported alongside this.\n\n###### `destroy(identity)`\n\nThis must be called by conventional components during [the `componentWillUnmount` lifecycle method](https://reactjs.org/docs/react-component.html#componentwillunmount).\n\n`DESTROY` is also exported alongside this.\n\n#### `withActionIdentity(actionCreator) =\u003e IdentityReceivingActionCreator`\n\nIf we choose to store the state within a redux store, we need to make sure that we can identify the state of each component by a key. Therefore, we should ensure that all actions contain an `identity` property.\n\nThis is a helper which can be used to wrap normal action creators with this extra property. The first argument of these wrapped action creators is always the `identity` property.\n\nIf you are using thunked actions or need more control for whatever reason, you can just conform to this type signature yourself. All you need to do is make sure that the first argument of each of your action creators is `identity` and that the action which is returned contains this value within an `identity` property.\n\n#### `withRenderProp(props)`\n\nThis is just a helper to improve the readability of [the render prop and function-as-a-child patterns](http://mxstbr.blog/2017/02/react-children-deepdive/#function-as-a-child).\n\n### Redux\n\n#### `asConnectedComponent(conventionalComponentConfiguration) =\u003e ConnectedComponent`\n\nTo generate a redux `ConnectedComponent` you just pass in the named exports of your conventional component.\n\nThe functions defined below are [used internally by this to ensure that there is a mapping](https://github.com/sebinsua/conventional-component/blob/master/src/asConnectedComponent.js) between a particular copy of the Component and its state.\n\n###### `createIdentifier(componentName, componentKey)`\n\n###### `createIdentifiedActionCreators(identifier, actionCreators) =\u003e Props =\u003e IdentifiedActionCreators`\n\n###### `createMapStateToProps(reducerName, identifier, structuredSelector)`\n\n#### `withReducerIdentity(identifierPredicate, identifiedReducer) =\u003e IdentifiedReducer`\n\nAs we need to be able to store the state of more than one copy of a particular component at a time, we need to make sure that the reducer which was previously written for a singular component is wrapped to understand the `identity` property of our actions. We pass this reducer in as the second argument (e.g. `identifiedReducer`).\n\nSince many actions could contain an `identity` property, we also need to make sure that we don't call the reducer unless the `identity` matches a predicate. Therefore the first argument (e.g. `identifierPredicate`) should either be the name of the component (e.g. `COMPONENT_NAME`) or a predicate function that returns a boolean.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsebinsua%2Fconventional-component","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsebinsua%2Fconventional-component","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsebinsua%2Fconventional-component/lists"}