├── .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 | [![Build Status](https://travis-ci.org/donavon/reclass.svg?branch=master)](https://travis-ci.org/donavon/reclass) 4 | [![npm version](https://img.shields.io/npm/v/reclass.svg)](https://www.npmjs.com/package/reclass) 5 | [![Coverage Status](https://coveralls.io/repos/github/donavon/reclass/badge.svg?branch=master)](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 | --------------------------------------------------------------------------------