├── .flowconfig
├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── __tests__
├── 01_basic_spec.js
├── 02_setter_spec.js
└── __snapshots__
│ └── 02_setter_spec.js.snap
├── dist
└── index.js
├── example
├── index.html
└── main.js
├── flow-typed
└── index.js
├── package-lock.json
├── package.json
└── src
├── index.d.ts
└── index.js
/.flowconfig:
--------------------------------------------------------------------------------
1 | [ignore]
2 |
3 | [include]
4 |
5 | [libs]
6 |
7 | [lints]
8 |
9 | [options]
10 |
11 | [strict]
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | npm-debug.log
4 | node_modules
5 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 6
4 | - 7
5 | - 8
6 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | ## [Unreleased]
4 |
5 | ## [1.6.0] - 2019-01-06
6 | ### Changed
7 | - no JSX in the library code
8 |
9 | ## [1.5.0] - 2018-10-18
10 | ### Added
11 | - naive type definition for TypeScript
12 |
13 | ## [1.4.0] - 2018-10-18
14 | ### Changed
15 | - update devDependencies
16 |
17 | ## [1.3.0] - 2018-09-17
18 | ### Changed
19 | - update devDependencies
20 |
21 | ## [1.2.0] - 2018-09-17
22 | ### Added
23 | - flow type annotation
24 |
25 | ## [1.1.0] - 2017-10-26
26 | ### Changed
27 | - update devDependencies
28 |
29 | ## [1.0.0] - 2017-09-13
30 | ### Added
31 | - options to configure state setters
32 | - fix the API with v1.0.0
33 |
34 | ## [0.2.0] - 2017-09-08
35 | ### Changed
36 | - update eslint packages
37 | - move to jest from mocha/chai
38 | - update React and use PureComponent
39 | - update babel packages
40 | - update webpack packages
41 |
42 | ## [0.1.2] - 2017-09-08
43 | ### Fixed
44 | - updated dist files
45 |
46 | ## [0.1.1] - 2016-05-05
47 | ### Fixed
48 | - shoudComponentUpdate uses shallowequal for better performance
49 |
50 | ## [0.1.0] - 2016-05-03
51 | ### Added
52 | - Initial release
53 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016-2019 Daishi Kato
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | react-compose-state
2 | ===================
3 |
4 | [](https://travis-ci.org/dai-shi/react-compose-state)
5 | [](https://badge.fury.io/js/react-compose-state)
6 | [](https://bundlephobia.com/result?p=react-compose-state)
7 |
8 | A helper function to attach state to
9 | stateless function components.
10 |
11 | Background
12 | ----------
13 |
14 | Since React v0.14, stateless function components are supported,
15 | which allows you to write a component as a pure function.
16 | This is very handy with ES2015 and JSX. Here is an example.
17 |
18 | ```javascript
19 | const Hello = ({ name }) => (
Hello, {name}!
);
20 | ```
21 |
22 | It is recommended to use stateless components as much as possible,
23 | and once you are used to it, you might want to avoid writing class-based components.
24 | Class-based components are powerful and you can mange lifecycles of components,
25 | but the state is one of what is required often, especially if using an external store (like Flux) is not an option.
26 |
27 | This package provides an easy way to add state to statelss components.
28 | This avoids the use of `this` which is also known as thisless javascript.
29 |
30 | Install
31 | -------
32 |
33 | ```bash
34 | npm install react-compose-state --save
35 | ```
36 |
37 | Usage
38 | -----
39 |
40 | One liner:
41 |
42 | ```javascript
43 | import React from 'react';
44 | import { composeWithState } from 'react-compose-state';
45 |
46 | const Counter = composeWithState({ counter: 1 })(({ counter, setCounter }) => (
47 |
48 | Count: {counter}
49 | setCounter(counter + 1)}>Click
50 |
51 | ));
52 | ```
53 |
54 | With PropTypes:
55 |
56 | ```javascript
57 | import React from 'react';
58 | import PropTypes from 'prop-types';
59 | import { composeWithState } from 'react-compose-state';
60 |
61 | const Counter = ({ counter, setCounter }) => (
62 |
63 | Count: {counter}
64 | setCounter(counter + 1)}>Click
65 |
66 | ));
67 |
68 | Counter.propTypes = {
69 | counter: PropTypes.number.isRequired,
70 | setCounter: PropTypes.func.isRequired,
71 | }
72 |
73 | const initialState = { counter: 1 };
74 |
75 | export default composeWithState(initialState)(Counter);
76 | ```
77 |
78 | With options:
79 |
80 | ```javascript
81 | import React from 'react';
82 | import PropTypes from 'prop-types';
83 | import { composeWithState } from 'react-compose-state';
84 |
85 | const Counter = ({ counter, updateCounter }) => (
86 |
87 | Count: {counter}
88 | updateCounter(counter + 1)}>Click
89 |
90 | ));
91 |
92 | Counter.propTypes = {
93 | counter: PropTypes.number.isRequired,
94 | updateCounter: PropTypes.func.isRequired,
95 | }
96 |
97 | const initialState = { counter: 1 };
98 | const options = {
99 | setters: {
100 | counter: 'updateCounter',
101 | },
102 | };
103 |
104 | export default composeWithState(initialState, options)(Counter);
105 | ```
106 |
107 | Example
108 | -------
109 |
110 | The [example](example) folder contains a working example.
111 | You can run it with
112 |
113 | ```bash
114 | PORT=8080 npm run example
115 | ```
116 |
117 | and open in your web browser.
118 |
--------------------------------------------------------------------------------
/__tests__/01_basic_spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-env jest */
2 |
3 | import React from 'react';
4 | import PropTypes from 'prop-types';
5 | import renderer from 'react-test-renderer';
6 | import { composeWithState } from '../src/index';
7 |
8 | describe('basic spec', () => {
9 | it('should have a compose function', () => {
10 | expect(composeWithState).toBeDefined();
11 | });
12 |
13 | it('should compose a component without state', () => {
14 | const BaseComponent = () => (
Base );
15 | const ComposedComponent = composeWithState()(BaseComponent);
16 |
17 | const actual = renderer.create( ).toJSON();
18 | const expected = renderer.create(
Base ).toJSON();
19 |
20 | expect(actual).toEqual(expected);
21 | });
22 |
23 | it('should compose a component with state', () => {
24 | const BaseComponent = ({ text }) => (
{text} );
25 | BaseComponent.propTypes = {
26 | text: PropTypes.string.isRequired,
27 | };
28 | const initialState = { text: 'hello' };
29 | const ComposedComponent = composeWithState(initialState)(BaseComponent);
30 |
31 | const actual = renderer.create( ).toJSON();
32 | const expected = renderer.create(
hello ).toJSON();
33 |
34 | expect(actual).toEqual(expected);
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/__tests__/02_setter_spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-env jest */
2 |
3 | import React from 'react';
4 | import PropTypes from 'prop-types';
5 | import { configure, mount } from 'enzyme';
6 | import toJson from 'enzyme-to-json';
7 | import Adapter from 'enzyme-adapter-react-15';
8 |
9 | import { composeWithState } from '../src/index';
10 |
11 | configure({ adapter: new Adapter() });
12 |
13 | describe('setter spec', () => {
14 | it('should update state with default setter', () => {
15 | const BaseComponent = ({ text, setText }) => (
16 |
17 |
{text}
18 | setText('replaced')}>button
19 |
20 | );
21 | BaseComponent.propTypes = {
22 | text: PropTypes.string.isRequired,
23 | setText: PropTypes.func.isRequired,
24 | };
25 | const initialState = { text: 'hello' };
26 | const ComposedComponent = composeWithState(initialState)(BaseComponent);
27 |
28 | const wrapper = mount( );
29 | expect(toJson(wrapper)).toMatchSnapshot();
30 | wrapper.find('button').simulate('click');
31 | expect(toJson(wrapper)).toMatchSnapshot();
32 | });
33 |
34 | it('should update state with custom setter', () => {
35 | const BaseComponent = ({ text, updateText }) => (
36 |
37 |
{text}
38 | updateText('replaced')}>button
39 |
40 | );
41 | BaseComponent.propTypes = {
42 | text: PropTypes.string.isRequired,
43 | updateText: PropTypes.func.isRequired,
44 | };
45 | const initialState = { text: 'hello' };
46 | const options = { setters: { text: 'updateText' } };
47 | const ComposedComponent = composeWithState(initialState, options)(BaseComponent);
48 |
49 | const wrapper = mount( );
50 | expect(toJson(wrapper)).toMatchSnapshot();
51 | wrapper.find('button').simulate('click');
52 | expect(toJson(wrapper)).toMatchSnapshot();
53 | });
54 | });
55 |
--------------------------------------------------------------------------------
/__tests__/__snapshots__/02_setter_spec.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`setter spec should update state with custom setter 1`] = `
4 | <_class>
5 |
9 |
10 |
11 | hello
12 |
13 |
17 | button
18 |
19 |
20 |
21 |
22 | `;
23 |
24 | exports[`setter spec should update state with custom setter 2`] = `
25 | <_class>
26 |
30 |
31 |
32 | replaced
33 |
34 |
38 | button
39 |
40 |
41 |
42 |
43 | `;
44 |
45 | exports[`setter spec should update state with default setter 1`] = `
46 | <_class>
47 |
51 |
52 |
53 | hello
54 |
55 |
59 | button
60 |
61 |
62 |
63 |
64 | `;
65 |
66 | exports[`setter spec should update state with default setter 2`] = `
67 | <_class>
68 |
72 |
73 |
74 | replaced
75 |
76 |
80 | button
81 |
82 |
83 |
84 |
85 | `;
86 |
--------------------------------------------------------------------------------
/dist/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.composeWithState = void 0;
7 |
8 | var _react = _interopRequireDefault(require("react"));
9 |
10 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
11 |
12 | function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
13 |
14 | function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; var ownKeys = Object.keys(source); if (typeof Object.getOwnPropertySymbols === 'function') { ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { return Object.getOwnPropertyDescriptor(source, sym).enumerable; })); } ownKeys.forEach(function (key) { _defineProperty(target, key, source[key]); }); } return target; }
15 |
16 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
17 |
18 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
19 |
20 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
21 |
22 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
23 |
24 | function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }
25 |
26 | function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
27 |
28 | function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
29 |
30 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
31 |
32 | function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
33 |
34 | /*::
35 | import type { StatelessFunctionalComponent } from 'react';
36 | */
37 | var isFunction = function isFunction(fn) {
38 | return typeof fn === 'function';
39 | };
40 |
41 | var capitalize = function capitalize(str) {
42 | return str.charAt(0).toUpperCase() + str.slice(1);
43 | };
44 |
45 | var composeWithState = function composeWithState(initialState
46 | /*: (Object | (props: Object) => Object)*/
47 | ) {
48 | var options
49 | /*: Object */
50 | = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
51 | return function (BaseComponent
52 | /*: StatelessFunctionalComponent */
53 | ) {
54 | return (
55 | /*#__PURE__*/
56 | function (_React$PureComponent) {
57 | _inherits(_class, _React$PureComponent);
58 |
59 | function _class(props
60 | /*: Object */
61 | ) {
62 | var _this;
63 |
64 | _classCallCheck(this, _class);
65 |
66 | _this = _possibleConstructorReturn(this, _getPrototypeOf(_class).call(this, props));
67 |
68 | if (isFunction(initialState)) {
69 | var initializeState = initialState
70 | /*: (props: Object) => Object */
71 | ;
72 | _this.state = initializeState(props);
73 | } else {
74 | _this.state = initialState;
75 | }
76 |
77 | if (!_this.state) {
78 | _this.state = {};
79 | }
80 |
81 | _this.stateSetters = {};
82 | var _options$setters = options.setters,
83 | setters = _options$setters === void 0 ? {} : _options$setters;
84 | Object.keys(_this.state).forEach(function (key) {
85 | var setter = setters[key] || "set".concat(capitalize(key));
86 |
87 | _this.stateSetters[setter] = function (val) {
88 | _this.setState(_defineProperty({}, key, val));
89 | };
90 | });
91 | return _this;
92 | }
93 | /*::
94 | stateSetters: {
95 | [string]: string => void,
96 | };
97 | */
98 |
99 |
100 | _createClass(_class, [{
101 | key: "render",
102 | value: function render() {
103 | return _react.default.createElement(BaseComponent, _objectSpread({}, this.props, this.state, this.stateSetters));
104 | }
105 | }]);
106 |
107 | return _class;
108 | }(_react.default.PureComponent
109 | /*:: */
110 | )
111 | );
112 | };
113 | };
114 |
115 | exports.composeWithState = composeWithState;
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | react-compose-state example
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/example/main.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 |
3 | import React from 'react';
4 | import ReactDOM from 'react-dom';
5 |
6 | import { composeWithState } from '../src/index';
7 |
8 | const Counter = composeWithState({ counter: 1 })(({ counter, setCounter }) => (
9 |
10 |
11 | Count:
12 | {counter}
13 |
14 | setCounter(counter + 1)}>+1
15 | setCounter(counter - 1)}>-1
16 |
17 | ));
18 |
19 | const TextBox = composeWithState({ text: '' })(({ text, setText }) => (
20 |
21 |
22 | Text:
23 | {text}
24 |
25 | setText(event.target.value)} />
26 |
27 | ));
28 |
29 | const App = () => (
30 |
31 |
Counter
32 |
33 |
34 | TextBox
35 |
36 |
37 |
38 | );
39 |
40 | ReactDOM.render( , document.getElementById('content'));
41 |
--------------------------------------------------------------------------------
/flow-typed/index.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | declare function composeWithState(initialState: Object | (props: Object) => Object, options?: Object): any;
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-compose-state",
3 | "description": "A helper function to attach state to stateless function components",
4 | "version": "1.6.1",
5 | "author": "Daishi Kato",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/dai-shi/react-compose-state.git"
9 | },
10 | "main": "./dist/index.js",
11 | "source": "./src/index.js",
12 | "types": "./src/index.d.ts",
13 | "scripts": {
14 | "compile": "babel src -d dist",
15 | "stop-flow": "flow stop",
16 | "test": "npm run eslint && npm run stop-flow && npm run flow && npm run jest",
17 | "eslint": "eslint src example __tests__",
18 | "flow": "flow",
19 | "jest": "jest",
20 | "example": "webpack-dev-server --port ${PORT:-8080} --entry ./example/main.js --output-filename bundle.js --content-base example --module-bind 'js=babel-loader' --mode development"
21 | },
22 | "keywords": [
23 | "react",
24 | "container",
25 | "state",
26 | "compose",
27 | "stateless",
28 | "thisless",
29 | "pure"
30 | ],
31 | "license": "MIT",
32 | "dependencies": {},
33 | "devDependencies": {
34 | "@babel/cli": "^7.2.3",
35 | "@babel/core": "^7.2.2",
36 | "@babel/preset-env": "^7.2.3",
37 | "@babel/preset-react": "^7.0.0",
38 | "babel-core": "^7.0.0-bridge.0",
39 | "babel-eslint": "^10.0.1",
40 | "babel-loader": "^8.0.5",
41 | "enzyme": "^3.8.0",
42 | "enzyme-adapter-react-15": "^1.2.0",
43 | "enzyme-to-json": "^3.3.5",
44 | "eslint": "^5.12.0",
45 | "eslint-config-airbnb": "^17.1.0",
46 | "eslint-plugin-import": "^2.14.0",
47 | "eslint-plugin-jsx-a11y": "^6.1.2",
48 | "eslint-plugin-react": "^7.12.3",
49 | "flow-bin": "^0.89.0",
50 | "jest": "^23.6.0",
51 | "prop-types": "^15.6.2",
52 | "react": "^15.6.2",
53 | "react-dom": "^15.6.2",
54 | "react-test-renderer": "^15.6.2",
55 | "webpack": "^4.28.3",
56 | "webpack-cli": "^3.2.0",
57 | "webpack-dev-server": "^3.1.14"
58 | },
59 | "peerDependencies": {
60 | "react": ">=15.6.2"
61 | },
62 | "eslintConfig": {
63 | "parser": "babel-eslint",
64 | "extends": [
65 | "airbnb"
66 | ],
67 | "rules": {
68 | "import/prefer-default-export": 0,
69 | "react/jsx-filename-extension": [
70 | 2,
71 | {
72 | "extensions": [
73 | ".js",
74 | ".jsx"
75 | ]
76 | }
77 | ],
78 | "import/no-extraneous-dependencies": [
79 | 2,
80 | {
81 | "devDependencies": true
82 | }
83 | ],
84 | "spaced-comment": 0,
85 | "arrow-parens": 0
86 | }
87 | },
88 | "babel": {
89 | "presets": [
90 | "@babel/preset-env",
91 | "@babel/preset-react"
92 | ]
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/index.d.ts:
--------------------------------------------------------------------------------
1 | type Mapper = (input: I) => O;
2 | type Updater = (f: ((v: T) => T) | T) => void;
3 | type Options = { setters: any };
4 | type GetterProps = {[K in keyof S]: S[K]};
5 | type SetterProps = any;
6 | export type ComposeWithState =
7 | (initialState: S | Mapper
, options?: Options) =>
8 | (base: React.ComponentType
) =>
9 | React.ComponentType
& SetterProps>;
10 | export const composeWithState: ComposeWithState;
11 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React from 'react';
4 | /*::
5 | import type { StatelessFunctionalComponent } from 'react';
6 | */
7 |
8 | const isFunction = fn => (typeof fn === 'function');
9 | const capitalize = str => (str.charAt(0).toUpperCase() + str.slice(1));
10 |
11 | export const composeWithState = (
12 | initialState /*: (Object | (props: Object) => Object)*/,
13 | options /*: Object */ = {},
14 | ) => (BaseComponent /*: StatelessFunctionalComponent */) => (
15 | class extends React.PureComponent/*:: */ {
16 | constructor(props /*: Object */) {
17 | super(props);
18 | if (isFunction(initialState)) {
19 | const initializeState = (initialState /*: (props: Object) => Object */);
20 | this.state = initializeState(props);
21 | } else {
22 | this.state = initialState;
23 | }
24 | if (!this.state) {
25 | this.state = {};
26 | }
27 | this.stateSetters = {};
28 | const { setters = {} } = options;
29 | Object.keys(this.state).forEach((key) => {
30 | const setter = setters[key] || `set${capitalize(key)}`;
31 | this.stateSetters[setter] = (val) => {
32 | this.setState({ [key]: val });
33 | };
34 | });
35 | }
36 |
37 | /*::
38 | stateSetters: {
39 | [string]: string => void,
40 | };
41 | */
42 |
43 | render() {
44 | return React.createElement(BaseComponent, {
45 | ...this.props,
46 | ...this.state,
47 | ...this.stateSetters,
48 | });
49 | }
50 | }
51 | );
52 |
--------------------------------------------------------------------------------