├── .babelrc
├── .eslintrc
├── .gitignore
├── .npmignore
├── .travis.yml
├── LICENSE
├── README.md
├── package.json
└── src
├── index.js
└── index.test.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "stage-2"]
3 | }
4 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "airbnb",
3 | "env": {
4 | "jest": true
5 | },
6 | "parser": "babel-eslint"
7 | }
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | lib/
2 | node_modules/
3 | coverage/
4 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | src/
2 | node_modules/
3 | coverage/
4 | *.test.js
5 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 |
2 | language: node_js
3 | node_js:
4 | - "6"
5 | script: npm run posttest
6 | after_success: npm run coveralls
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Donavon West
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 all
13 | 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 THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # reclass
2 |
3 | [](https://travis-ci.org/donavon/reclass)
4 | [](https://www.npmjs.com/package/reclass)
5 | [](https://coveralls.io/github/donavon/reclass?branch=master)
6 |
7 | TL;DR
8 |
9 | * Write React stateful components without worrying about `this` or binding methods.
10 | * No more `this.myMethod = this.myMethod.bind(this);`. Whatz!?
11 | * Write your methods using ES6 "fat arrow" functions.
12 | * A no-frill/smaller footprint alternative to
13 | [recompose](https://github.com/acdlite/recompose).
14 |
15 | ## Install
16 | ```bash
17 | $ npm i --save reclass
18 | ```
19 |
20 | ## Usage
21 |
22 | ```js
23 | import React from 'react';
24 | import reclass from 'reclass';
25 |
26 | const MyComponent = (ctx) => {
27 | const { setState } = ctx;
28 |
29 | const clickHandler = () => {
30 | setState({...});
31 | };
32 |
33 | const render = (props, state, context) => (
34 | ...
35 |
36 | );
37 |
38 | return {
39 | state: {...},
40 | render,
41 | };
42 | };
43 |
44 | export default reclass(MyComponent);
45 | ```
46 |
47 | Let's break it down...
48 |
49 | First of all, you write your stateful component as a simple JavaScript function and NOT an ES6 class.
50 | This function is responsible for doing a few things:
51 |
52 | - Accepts a `ctx` context object as an argument (see explaination of `ctx` below).
53 |
54 | ```js
55 | const MyComponent = (ctx) => {
56 | ```
57 |
58 | - Returns a `properties` object which defines your component. At a minimun, you'll probably want to return a `render` function and a `state` object that represents your component's initial state.
59 |
60 | ```js
61 | return { // Return initial state and public methods
62 | state: {...}, // Initial state
63 | render,
64 | };
65 | ```
66 |
67 | - Exposes optional static `propTypes` and `defaultProps` properties.
68 |
69 | ```js
70 | MyComponent.propTypes = {
71 | ...
72 | };
73 |
74 | MyComponent.defaultProps = {
75 | ...
76 | };
77 | ```
78 |
79 | Before you export your component, you wrap it using `reclass`.
80 | ```js
81 | export default reclass(MyComponent);
82 | ```
83 |
84 | ### The `ctx` argument
85 |
86 | You will be passed a `ctx` object with the following properties and methods:
87 |
88 | - **setState** - This is the same `setState` that you are used to, but you can forget the `this`.
89 |
90 | >💡 Pro Tip: Destructure `setState` for ease of use.
91 |
92 | Example:
93 | ```js
94 | const MyComponent = (ctx) => {
95 | const { setState } = ctx;
96 | ...
97 | setState({ partial });
98 | ```
99 |
100 | - **state** - A getter/setter for `state`. As a getter, this is same as `this.state`.
101 | Note that both `props` are `state` and passed to `render`,
102 | so, depending on your applications you may not need to access these from `ctx` directly.
103 |
104 | >💡 Pro Tip: With `reclass`, you can set `state` directly without consequence. You can use this instead of calling `setState`.
105 |
106 | Example:
107 | ```js
108 | const MyComponent = (ctx) => {
109 | ...
110 | ctx.state = { partial }; // same as setState({ partial });
111 | ```
112 |
113 | - **props** - A getter for `props`.
114 |
115 | - **context** - A getter for `context`.
116 |
117 | ## render
118 |
119 | In all likelihood, you will have a `render` method.
120 | `render` works exactly as you might expect, except that you are passed
121 | `props`, `state`, and `context`.
122 |
123 | >💡 Pro Tip: You can destructure `props`, `state`, and `context` inline as parameters.
124 |
125 | Example:
126 | ```js
127 | const render = ({ someProp }, { someStateValue }) => (
128 | ...
129 | );
130 | ```
131 |
132 | You will also notice that your `render` method (all methods for that fact)
133 | can be written in ES6 "fat arrow" style.
134 |
135 | ## Example
136 |
137 | Below is an example of a 'Greeter' component written using `reclass`.
138 | You can see it running live on
139 | [CodeSandbox](https://codesandbox.io/s/DRz5p2W8y).
140 |
141 | ```js
142 | import React from 'react';
143 | import PropTypes from 'prop-types';
144 | import reclass from 'reclass';
145 | import GreeterView from './GreeterView';
146 |
147 | const Greeter = (ctx) => {
148 | const changeName = (name) => {
149 | ctx.state = { name };
150 | };
151 |
152 | const render = ({ greeting }, { name }) => (
153 |
158 | );
159 |
160 | return {
161 | state: { name: 'John' }, // Initial state
162 | render,
163 | };
164 | };
165 |
166 | Greeter.propTypes = {
167 | greeting: PropTypes.string,
168 | };
169 |
170 | Greeter.defaultProps = {
171 | greeting: '',
172 | };
173 |
174 | export default reclass(Greeter);
175 | ```
176 |
177 | ## Really?
178 |
179 | OK. Adding all of that code in a closure doesn't come without a price.
180 | It can use more memory than class-based components where methods are on the prototype
181 | and not on each class instance. There are a few things that make this moot.
182 |
183 | If you have a component that has hundreds or even thousands of instances, this will be a factor.
184 | But if you have only a few instances, or even a single instance, this isn't a concern.
185 |
186 | In any case, I suggest that you follow the
187 | [Single Responsibility Principle](https://en.wikipedia.org/wiki/Single_responsibility_principle) and break out
188 | the rendering responsibility from the state management responsibility.
189 | Rendering, therefore, can he handled in a
190 | stateless functional component. See an example of this
191 | [here](https://codesandbox.io/s/DRz5p2W8y).
192 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reclass",
3 | "version": "0.2.1",
4 | "description": "Write React stateful components without worrying about this.",
5 | "main": "./lib/index.js",
6 | "scripts": {
7 | "prepublish": "npm run lint && npm run _build && npm run _test",
8 | "build": "npm run lint && npm run _build",
9 | "_build": "babel src --out-dir lib --ignore '**/*.test.js'",
10 | "test": "npm run lint && npm run _test",
11 | "posttest": "cowsay Your tests all passed!",
12 | "_test": "jest",
13 | "test:watch": "jest --watch",
14 | "coveralls": "cat ./coverage/lcov.info | coveralls",
15 | "lint": "eslint src"
16 | },
17 | "repository": {
18 | "type": "git",
19 | "url": "git+https://github.com/donavon/reclass.git"
20 | },
21 | "keywords": [
22 | "react",
23 | "class",
24 | "this",
25 | "component"
26 | ],
27 | "contributors": [
28 | "Donavon West (http://donavon.com)",
29 | "Yassine ELOUAFI "
30 | ],
31 | "license": "MIT",
32 | "bugs": {
33 | "url": "https://github.com/donavon/reclass/issues"
34 | },
35 | "homepage": "https://github.com/donavon/reclass#readme",
36 | "peerDependencies": {
37 | "react": "*"
38 | },
39 | "devDependencies": {
40 | "babel-cli": "^6.24.1",
41 | "babel-eslint": "^7.2.3",
42 | "babel-preset-es2015": "^6.24.1",
43 | "babel-preset-stage-2": "^6.24.1",
44 | "coveralls": "^2.13.1",
45 | "cowsay": "^1.1.9",
46 | "eslint": "^3.19.0",
47 | "eslint-config-airbnb": "^15.0.1",
48 | "eslint-plugin-import": "^2.3.0",
49 | "eslint-plugin-jsx-a11y": "^5.0.3",
50 | "eslint-plugin-react": "^7.1.0",
51 | "jest": "^20.0.4",
52 | "react": "^15.6.1"
53 | },
54 | "jest": {
55 | "verbose": true,
56 | "collectCoverage": true,
57 | "collectCoverageFrom": [
58 | "src/*.js"
59 | ],
60 | "coverageThreshold": {
61 | "global": {
62 | "statements": 0,
63 | "branches": 0,
64 | "functions": 0,
65 | "lines": 0
66 | }
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const buildCtx = (that) => {
4 | const setState = that.setState.bind(that);
5 | return {
6 | setState,
7 | get state() { return that.state; },
8 | set state(value) { setState(value); },
9 | get props() { return that.props; },
10 | get context() { return that.context; },
11 | };
12 | };
13 |
14 | export default (factory) => {
15 | const Component = class extends React.Component {
16 | static displayName = factory.name;
17 |
18 | constructor(...args) {
19 | super(...args);
20 | const ctx = buildCtx(this);
21 | const { render, ...properties } = factory(ctx);
22 | Object.assign(this, properties);
23 | this.render = (...futureArgs) => (
24 | render(this.props, this.state, this.context, ...futureArgs)
25 | );
26 | }
27 | };
28 |
29 | Object.assign(Component, factory);
30 |
31 | return Component;
32 | };
33 |
--------------------------------------------------------------------------------
/src/index.test.js:
--------------------------------------------------------------------------------
1 | import reclass from './';
2 |
3 | let myComponentProps;
4 | let myComponentState;
5 | let myComponentContext;
6 | let ctx;
7 | const state = { foo: 'bar' };
8 |
9 | const MyComponent = (that) => {
10 | ctx = that;
11 |
12 | return {
13 | state,
14 | otherPro: 'otherPro',
15 | render: (renderProps, renderState, renderContext) => {
16 | myComponentProps = renderProps;
17 | myComponentState = renderState;
18 | myComponentContext = renderContext;
19 | return null;
20 | },
21 | };
22 | };
23 | MyComponent.staticProp = 'hello';
24 |
25 | describe('reclass', () => {
26 | test('is a function', () => {
27 | expect(typeof reclass).toBe('function');
28 | });
29 | describe('when passed a factory component', () => {
30 | const Reclassed = reclass(MyComponent);
31 | test('returns a class', () => {
32 | expect(typeof Reclassed).toBe('function');
33 | });
34 | test('has a "displayName" static property', () => {
35 | expect('displayName' in Reclassed).toBe(true);
36 | expect(Reclassed.displayName).toBe('MyComponent');
37 | });
38 | test('has all static properties of the wrapped component', () => {
39 | expect('staticProp' in Reclassed).toBe(true);
40 | expect(Reclassed.staticProp).toBe(MyComponent.staticProp);
41 | });
42 | describe('that when instanciated with MyComponent', () => {
43 | const props = {};
44 | const context = {};
45 | const instance = new Reclassed(props, context);
46 | test('returns an instance', () => {
47 | expect(typeof instance).toBe('object');
48 | });
49 | describe('MyComponent is called with ctx', () => {
50 | test('ctx is an object', () => {
51 | expect(typeof ctx).toBe('object');
52 | });
53 | test('with a "setState" function', () => {
54 | expect('setState' in ctx).toBe(true);
55 | expect(typeof ctx.setState).toBe('function');
56 | });
57 | test('with a "props" getter', () => {
58 | expect('props' in ctx).toBe(true);
59 | expect(ctx.props).toBe(props);
60 | });
61 | test('with a "state" getter', () => {
62 | expect('state' in ctx).toBe(true);
63 | expect(ctx.state).toBe(state);
64 | });
65 | test('with a "state" setter', () => {
66 | // NOTE this produces a warning and is a noop
67 | // eslint-disable-next-line no-console
68 | console.log('*** IGNORE the following error. It is expected. ***');
69 | ctx.state = { foo: 'baz' };
70 | expect(ctx.state).toBe(state);
71 | });
72 | test('with a "context" getter', () => {
73 | expect('context' in ctx).toBe(true);
74 | expect(ctx.context).toBe(context);
75 | });
76 | });
77 | describe('with a "context" property', () => {
78 | test('equal to that sent to the constructor', () => {
79 | expect('context' in instance).toBe(true);
80 | expect(instance.context).toBe(context);
81 | });
82 | });
83 | describe('with a "props" property', () => {
84 | test('equal to that sent to the constructor', () => {
85 | expect('context' in instance).toBe(true);
86 | expect(instance.props).toBe(props);
87 | });
88 | });
89 | describe('with a "refs" property', () => {
90 | test('that is an object', () => {
91 | expect('refs' in instance).toBe(true);
92 | expect(typeof instance.refs).toBe('object');
93 | });
94 | });
95 | describe('with a custom "otherPro" property', () => {
96 | test('that is a string', () => {
97 | expect('otherPro' in instance).toBe(true);
98 | expect(instance.otherPro).toBe('otherPro');
99 | });
100 | });
101 | describe('with a "render" key', () => {
102 | test('that is a function', () => {
103 | expect('render' in instance).toBe(true);
104 | expect(typeof instance.render).toBe('function');
105 | });
106 | const output = instance.render();
107 | describe('when called', () => {
108 | test('it returns rendered output of wrapped component', () => {
109 | expect(output).toBe(null);
110 | });
111 | test('it passes "props" to wrapped component', () => {
112 | expect(myComponentProps).toBe(props);
113 | });
114 | test('it passes "state" to wrapped component', () => {
115 | expect(myComponentState).toBe(state);
116 | });
117 | test('it passes "context" to wrapped component', () => {
118 | expect(myComponentContext).toBe(context);
119 | });
120 | });
121 | });
122 | describe('with a "updater" key', () => {
123 | test('that is an object', () => {
124 | expect('updater' in instance).toBe(true);
125 | expect(typeof instance.updater).toBe('object');
126 | });
127 | });
128 | });
129 | });
130 | });
131 |
--------------------------------------------------------------------------------