├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── .watchmanconfig ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── rollup.config.js └── src ├── createReactAF.js ├── createReactAF.test.js ├── index.js ├── utils.js └── utils.test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"], 3 | "plugins": ["transform-object-rest-spread"] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "env": { 5 | "jest": true 6 | }, 7 | "rules": { 8 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | dist/ 3 | node_modules/ 4 | npm-debug.log* 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | src/ 3 | node_modules/ 4 | npm-debug.log* 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "lts/*" 5 | 6 | script: npm test 7 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donavon/react-af/a506c808752d32250f078b03ac05ab2e153645f9/.watchmanconfig -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018, 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-af 2 | [![Build Status](https://travis-ci.org/donavon/react-af.svg?branch=master)](https://travis-ci.org/donavon/react-af) [![npm version](https://img.shields.io/npm/v/react-af.svg)](https://www.npmjs.com/package/react-af) 3 | 4 | ![React AF graffiti wall](https://user-images.githubusercontent.com/887639/37485417-c7c50fb8-2861-11e8-8e64-363c0e02372a.png) 5 | 6 | ## TL;DR 7 | 8 | - Allows you to code using certain React.next features today! 9 | - Perfect for component library maintainers. 10 | - It does for React what Babel does for JavaScript. 11 | - Support `getDerivedStateFromProps` on older versions of React. 12 | - Supports `Fragment` on older versions of React. 13 | - Supports `createContext` (the new context API) on older versions of React. 14 | 15 | ## What is this project? 16 | 17 | Starting with React 17, several class component lifecycles will be deprecated: 18 | `componentWillMount`, `componentWillReceiveProps`, and `componentWillUpdate` (see [React RFC 6](https://github.com/reactjs/rfcs/pull/6)). 19 | 20 | One problem that React component library developers face is that they don't control the version of React that they run on — 21 | this is controlled by the consuming application. 22 | This leaves library developers in a bit of a quandary. 23 | Should they use feature detection or 24 | code to the lowest denominator? 25 | 26 | `react-af` emulates newer features of React on older versions, 27 | allowing developers to concentrate on the business problem 28 | and not the environment. 29 | 30 | ## Install 31 | 32 | Install `react-af` using npm: 33 | ```sh 34 | $ npm install react-af --save 35 | ``` 36 | 37 | or with Yarn: 38 | ```sh 39 | $ yarn add react-af 40 | ``` 41 | 42 | ## Import 43 | 44 | In your code, all you need to do is change the React import from this: 45 | ```js 46 | import React from 'react'; 47 | ``` 48 | 49 | To this: 50 | ```js 51 | import React from 'react-af'; 52 | ``` 53 | 54 | That's it! You can now code your library components as though 55 | they are running on a modern React (not all features supported... yet), 56 | even though your code may be running on an older version. 57 | 58 | `react-af` imports from `react` under the hood 59 | (it has a `peerDependency` of React >=15), 60 | patching or passing through features where necessary. 61 | 62 | ## API 63 | 64 | Here are the modern React features that you can use, even if yur code is running 65 | on older version of React 15 or React 16. 66 | 67 | ### `getDerivedStateFromProps` 68 | 69 | `react-af` supports new static lifecycle `getDerivedStateFromProps`. 70 | 71 | Here is an example component written using 72 | `componentWillReceiveProps`. 73 | ```js 74 | class ExampleComponent extends React.Component { 75 | state = { text: this.props.text }; 76 | 77 | componentWillReceiveProps(nextProps) { 78 | if (this.props.text !== nextProps.text) { 79 | this.setState({ 80 | text: nextProps.text 81 | }); 82 | } 83 | } 84 | } 85 | ``` 86 | 87 | And here it is after converting to be compatible with modern React. 88 | ```js 89 | class ExampleComponent extends React.Component { 90 | state = {}; 91 | 92 | static getDerivedStateFromProps(nextProps, prevState) { 93 | return prevState.text !== nextProps.text 94 | ? { 95 | text: nextProps.text 96 | } 97 | : null; 98 | } 99 | } 100 | ``` 101 | 102 | ### Fragment 103 | 104 | Starting with React 16.2, there is a new `` component 105 | that allows you to return multiple children. 106 | Prior to 16.2, you needed to wrap multiple children in a wrapping `div`. 107 | 108 | With `react-af`, you can use `React.Fragment` on older versions of React as well. 109 | 110 | ```jsx 111 | import React, { Fragment } from 'react-af'; 112 | 113 | const Weather = ({ city, degrees }) => ( 114 | 115 |
{city}
116 |
{degrees}℉
117 |
118 | ); 119 | ``` 120 | 121 | The code above works natively in React 16.2 and greater. 122 | In lesser versions of React, `Fragment` is replaced with a `div` automatically. 123 | 124 | ### createContext 125 | 126 | React 16.3 also added support for the new context API. 127 | Well `react-af` supports that as well. 128 | 129 | Here's an example take from Kent Dodds's article 130 | [React’s new Context API](https://medium.com/dailyjs/reacts-%EF%B8%8F-new-context-api-70c9fe01596b). 131 | 132 | ```js 133 | import React, { createContext, Component } from 'react-af'; 134 | 135 | const ThemeContext = createContext('light') 136 | class ThemeProvider extends Component { 137 | state = {theme: 'light'} 138 | render() { 139 | return ( 140 | 141 | {this.props.children} 142 | 143 | ) 144 | } 145 | } 146 | class App extends Component { 147 | render() { 148 | return ( 149 | 150 | 151 | {val =>
{val}
} 152 |
153 |
154 | ) 155 | } 156 | } 157 | ``` 158 | 159 | ## Other projects 160 | 161 | ### `react-lifecycles-compat` 162 | 163 | You might also want to take a look at 164 | `react-lifecycles-compat` by the 165 | [React team](https://github.com/reactjs/react-lifecycles-compat). 166 | It doesn't support `Fragment` or `createContext` and it requires additional 167 | plumbing to setup, but it's lighter and may be adequate for some projets. 168 | 169 | ### `create-react-context` 170 | 171 | If all you need is context support, consider using 172 | [`create-react-context`](https://github.com/jamiebuilds/create-react-context), 173 | which is what this package uses to emulate `createContext()`. 174 | 175 | ## What's with the name? 176 | 177 | ReactAF stands for React Always Fresh (or React As F&#%!). 178 | Your choice. 179 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-af", 3 | "version": "0.2.2", 4 | "description": "Code using modern React features today! It does for React what Babel does for JavaScript.", 5 | "author": "Donavon West (http://donavon.com)", 6 | "main": "dist/react-af.cjs.js", 7 | "jsnext:main": "dist/react-af.es.js", 8 | "module": "dist/react-af.es.js", 9 | "scripts": { 10 | "prepublishOnly": "npm test && npm run build", 11 | "build": "NODE_ENV=development rollup -c", 12 | "prebuild": "rimraf dist", 13 | "lint": "eslint src", 14 | "pretest": "npm run lint", 15 | "test": "NODE_ENV=development jest" 16 | }, 17 | "keywords": [ 18 | "react", 19 | "component", 20 | "createContext", 21 | "getDerivedStateFromProps", 22 | "Fragment", 23 | "polyfill" 24 | ], 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/donavon/react-af.git" 28 | }, 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/donavon/react-af/issues" 32 | }, 33 | "homepage": "https://github.com/donavon/react-af#readme", 34 | "peerDependencies": { 35 | "react": ">=15.0.0" 36 | }, 37 | "devDependencies": { 38 | "babel-cli": "^6.26.0", 39 | "babel-eslint": "^8.2.2", 40 | "babel-plugin-external-helpers": "^6.22.0", 41 | "babel-plugin-transform-class-properties": "^6.24.1", 42 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.0", 43 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 44 | "babel-preset-env": "^1.6.1", 45 | "babel-preset-react": "^6.24.1", 46 | "eslint": "^4.18.2", 47 | "eslint-config-airbnb": "^16.1.0", 48 | "eslint-plugin-import": "^2.9.0", 49 | "eslint-plugin-jsx-a11y": "^6.0.3", 50 | "eslint-plugin-react": "^7.7.0", 51 | "jest": "^22.4.2", 52 | "react": "^16.2.0", 53 | "react-dom": "^16.2.0", 54 | "rimraf": "^2.6.2", 55 | "rollup": "^0.56.5", 56 | "rollup-plugin-babel": "^3.0.3", 57 | "rollup-plugin-commonjs": "^9.1.0", 58 | "rollup-plugin-flow": "^1.1.1", 59 | "rollup-plugin-json": "^2.3.0", 60 | "rollup-plugin-node-resolve": "^3.2.0", 61 | "rollup-plugin-sourcemaps": "^0.4.2", 62 | "rollup-plugin-uglify": "^3.0.0" 63 | }, 64 | "dependencies": { 65 | "create-react-context": "^0.2.1" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable flowtype/require-valid-file-annotation, no-console, import/extensions */ 2 | import nodeResolve from 'rollup-plugin-node-resolve'; 3 | import commonjs from 'rollup-plugin-commonjs'; 4 | import babel from 'rollup-plugin-babel'; 5 | import json from 'rollup-plugin-json'; 6 | import flow from 'rollup-plugin-flow'; 7 | import sourceMaps from 'rollup-plugin-sourcemaps'; 8 | import uglify from 'rollup-plugin-uglify'; 9 | import pkg from './package.json'; 10 | 11 | // const cjs = { 12 | // format: 'cjs', 13 | // exports: 'named', 14 | // }; 15 | 16 | const commonPlugins = [ 17 | flow({ 18 | pretty: true, // Needed for sourcemaps to be properly generated. 19 | }), 20 | json(), 21 | nodeResolve(), 22 | sourceMaps(), 23 | commonjs({ 24 | ignoreGlobal: true, 25 | }), 26 | babel({ 27 | exclude: 'node_modules/**', 28 | babelrc: false, 29 | presets: [ 30 | ['env', { modules: false }], 31 | ], 32 | plugins: [ 33 | 'transform-object-rest-spread', 34 | 'external-helpers', 35 | ], 36 | }), 37 | ]; 38 | 39 | if (process.env.NODE_ENV === 'production') { 40 | commonPlugins.push(uglify()); 41 | } 42 | 43 | const configBase = { 44 | input: 'src/index.js', 45 | external: ['react'].concat(Object.keys(pkg.dependencies || {})), 46 | plugins: commonPlugins, 47 | }; 48 | 49 | const esConfig = Object.assign({}, configBase, { 50 | output: { 51 | format: 'es', 52 | file: pkg.module, 53 | globals: { react: 'React' }, 54 | // sourcemap: true, 55 | }, 56 | }); 57 | 58 | const cjsConfig = Object.assign({}, configBase, { 59 | output: { 60 | format: 'cjs', 61 | file: pkg.main, 62 | exports: 'named', 63 | globals: { react: 'React' }, 64 | // sourcemap: true, 65 | }, 66 | }); 67 | 68 | // const otherConfig = Object.assign({}, configBase, { 69 | // output: [ 70 | // { 71 | // format: 'es', 72 | // file: pkg.module, 73 | // globals: { react: 'React' }, 74 | // sourcemap: true, 75 | // }, 76 | // { 77 | // format: 'cjs', 78 | // file: pkg.main, 79 | // exports: 'named', 80 | // }, 81 | // ], 82 | // }); 83 | 84 | export default [ 85 | esConfig, 86 | cjsConfig, 87 | ]; 88 | -------------------------------------------------------------------------------- /src/createReactAF.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-multi-comp */ 2 | import createReactContext from 'create-react-context'; 3 | import { objectWithoutProperties } from './utils'; 4 | 5 | const EmulatedFragment = 'div'; 6 | 7 | function getDerivedStateFromProps(instance, props, prevState) { 8 | const state = instance.constructor.getDerivedStateFromProps(props, prevState); 9 | if (state) { 10 | instance.setState(state); 11 | } 12 | } 13 | 14 | function componentWillMount() { 15 | getDerivedStateFromProps(this, this.props, this.state); 16 | } 17 | // eslint-disable-next-line no-underscore-dangle 18 | componentWillMount.__suppressDeprecationWarning = true; 19 | 20 | function componentWillReceiveProps(nextProps) { 21 | getDerivedStateFromProps(this, nextProps, this.state); 22 | } 23 | // eslint-disable-next-line no-underscore-dangle 24 | componentWillReceiveProps.__suppressDeprecationWarning = true; 25 | 26 | function setMethodSafe(instance, method, fn) { 27 | if (instance[method]) { 28 | throw new Error(`[${instance.constructor.name}] ${method} has been deprecated`); 29 | } 30 | // eslint-disable-next-line no-param-reassign 31 | instance[method] = fn; 32 | } 33 | 34 | function enhanceComponent(instance) { 35 | if (instance.constructor.getDerivedStateFromProps) { 36 | setMethodSafe(instance, 'componentWillMount', componentWillMount); 37 | setMethodSafe(instance, 'componentWillReceiveProps', componentWillReceiveProps); 38 | } 39 | } 40 | 41 | const createReactAF = React => ( 42 | React.StrictMode // 16.3 and above? 43 | ? React // Return as-is. 44 | : { 45 | ...(objectWithoutProperties(React, ['PropTypes', 'createClass'])), // So 15.x doesn't warn. 46 | Component: 47 | class ReactAFComponent extends React.Component { 48 | constructor(...args) { 49 | super(...args); 50 | enhanceComponent(this); 51 | } 52 | }, 53 | PureComponent: 54 | class ReactAFPureComponent extends React.PureComponent { 55 | constructor(...args) { 56 | super(...args); 57 | enhanceComponent(this); 58 | } 59 | }, 60 | isGetDerivedStateFromPropsEmulated: true, 61 | Fragment: React.Fragment || EmulatedFragment, 62 | isFragmentEmulated: !React.Fragment, 63 | createContext: React.createContext || createReactContext, 64 | isCreateContextEmulated: !React.createContext, 65 | } 66 | ); 67 | 68 | export default createReactAF; 69 | -------------------------------------------------------------------------------- /src/createReactAF.test.js: -------------------------------------------------------------------------------- 1 | import createReactAF from './createReactAF'; 2 | 3 | const Fragment = 'fragment'; 4 | 5 | class ComponentStub {} 6 | 7 | const React15xStub = { 8 | Component: ComponentStub, 9 | PureComponent: ComponentStub, 10 | PropTypes: true, 11 | createClass: true, 12 | other: 'other', 13 | }; 14 | 15 | const React162Stub = { 16 | Component: ComponentStub, 17 | PureComponent: ComponentStub, 18 | Fragment, 19 | }; 20 | 21 | const React163Stub = { 22 | StrictMode: true, 23 | }; 24 | 25 | describe('createReactAF', () => { 26 | describe('when passed React 15.x', () => { 27 | const ReactAF = createReactAF(React15xStub); 28 | 29 | const component = new ReactAF.Component(); 30 | const pureComponent = new ReactAF.PureComponent(); 31 | 32 | test('isGetDerivedStateFromPropsEmulated is set to true', () => { 33 | expect(ReactAF.isGetDerivedStateFromPropsEmulated).toBe(true); 34 | }); 35 | test('isFragmentEmulated is set to true', () => { 36 | expect(ReactAF.isFragmentEmulated).toBe(true); 37 | }); 38 | test('createContext is emulated"', () => { 39 | expect(typeof ReactAF.createContext).toBe('function'); 40 | expect(ReactAF.isCreateContextEmulated).toBe(true); 41 | }); 42 | test('Fragment is emulated"', () => { 43 | expect(ReactAF.Fragment).toBe('div'); 44 | }); 45 | test('PropTypes is undefined"', () => { 46 | expect(ReactAF.PropTypes).toBe(undefined); 47 | }); 48 | test('createClass is undefined"', () => { 49 | expect(ReactAF.createClass).toBe(undefined); 50 | }); 51 | test('others properties/methods are passed through"', () => { 52 | expect(ReactAF.other).toBe(React15xStub.other); 53 | }); 54 | test('ReactAF.Component inherits from React.Component', () => { 55 | expect(component instanceof React15xStub.Component).toBe(true); 56 | }); 57 | test('ReactAF.PureComponent inherits from React.PureComponent', () => { 58 | expect(pureComponent instanceof React15xStub.PureComponent).toBe(true); 59 | }); 60 | }); 61 | 62 | describe('when passed React 16.2', () => { 63 | const ReactAF = createReactAF(React162Stub); 64 | 65 | test('isGetDerivedStateFromPropsEmulated is set to true', () => { 66 | expect(!!ReactAF.isGetDerivedStateFromPropsEmulated).toBe(true); 67 | }); 68 | test('isFragmentEmulated is set to false', () => { 69 | expect(!!ReactAF.isFragmentEmulated).toBe(false); 70 | }); 71 | test('Fragment is NOT emulated"', () => { 72 | expect(ReactAF.Fragment).toBe(React162Stub.Fragment); 73 | }); 74 | }); 75 | 76 | describe('when passed React 16.3', () => { 77 | const ReactAF = createReactAF(React163Stub); 78 | 79 | test('React is returned as-is', () => { 80 | expect(ReactAF).toBe(React163Stub); 81 | }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import createReactAF from './createReactAF'; 3 | 4 | const ReactAF = createReactAF(React); 5 | 6 | module.exports = ReactAF.default || ReactAF; 7 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | 3 | const setKeyOnTarget = setFn => (target, key) => { 4 | // eslint-disable-next-line no-param-reassign 5 | target[key] = setFn(key); 6 | return target; 7 | }; 8 | 9 | export const objectWithoutProperties = (obj, keysArr) => { 10 | const keys = keysArr.reduce(setKeyOnTarget(key => key), {}); 11 | return Object.keys(obj) 12 | .filter(key => !keys[key]) 13 | .reduce(setKeyOnTarget(key => obj[key]), {}); 14 | }; 15 | -------------------------------------------------------------------------------- /src/utils.test.js: -------------------------------------------------------------------------------- 1 | import { objectWithoutProperties } from './utils'; 2 | 3 | const testObj = { 4 | a: 1, 5 | b: 2, 6 | c: 3, 7 | }; 8 | 9 | describe('utils', () => { 10 | describe('objectWithoutProperties', () => { 11 | test('remove a property', () => { 12 | const obj = objectWithoutProperties(testObj, ['a']); 13 | expect(obj).toEqual({ b: 2, c: 3 }); 14 | }); 15 | test('remove all properties', () => { 16 | const obj = objectWithoutProperties(testObj, Object.keys(testObj)); 17 | expect(obj).toEqual({}); 18 | }); 19 | test('remove nothing', () => { 20 | const obj = objectWithoutProperties(testObj, []); 21 | expect(obj).toEqual(testObj); 22 | }); 23 | test('remove unknown property', () => { 24 | const obj = objectWithoutProperties(testObj, ['d']); 25 | expect(obj).toEqual(testObj); 26 | }); 27 | }); 28 | }); 29 | --------------------------------------------------------------------------------