├── .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 | [](https://travis-ci.org/donavon/react-af) [](https://www.npmjs.com/package/react-af)
3 |
4 | 
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 |
--------------------------------------------------------------------------------