234 | );
235 | }
236 | }
237 |
238 | createMethod(() => {});
239 | createRender(() => {});
240 | createValue(() => {});
241 |
242 | export default hot(module)(App);
243 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | // external dependencies
2 | import React from 'react';
3 | import {findDOMNode} from 'react-dom';
4 |
5 | // utils
6 | import {
7 | IGNORED_COMPONENT_KEYS,
8 | IGNORED_STATIC_KEYS,
9 | addPropTypeIsRequired,
10 | bindMethods,
11 | createRefCreator,
12 | getNamespacedRef,
13 | identity,
14 | isClassComponent,
15 | logInvalidInstanceError,
16 | } from './utils';
17 |
18 | /**
19 | * @function createCombinedRef
20 | *
21 | * @description
22 | * create a ref that assigns both the raw component and the underlying HTML element to the instance on a namespace
23 | *
24 | * @param {ReactComponent} instance the instance to assign to
25 | * @param {string} ref the instance value name
26 | * @returns {{component: ReactComponent, element: HTMLElement}} the combined ref
27 | */
28 | export const createCombinedRef = createRefCreator(getNamespacedRef);
29 |
30 | /**
31 | * @function createComponentRef
32 | *
33 | * @description
34 | * create a ref that assigns the component itself to the instance
35 | *
36 | * @param {ReactComponent} instance the instance to assign to
37 | * @param {string} ref the instance value name
38 | * @returns {ReactComponent} the component ref
39 | */
40 | export const createComponentRef = createRefCreator(identity);
41 |
42 | /**
43 | * @function createElementRef
44 | *
45 | * @description
46 | * create a ref that assigns the component's underlying HTML element to the instance
47 | *
48 | * @param {ReactComponent} instance the instance to assign to
49 | * @param {string} ref the instance value name
50 | * @returns {HTMLElement} the element ref
51 | */
52 | export const createElementRef = createRefCreator(findDOMNode);
53 |
54 | /**
55 | * @function createMethod
56 | *
57 | * @description
58 | * create a method that is a pure version of the lifecycle / instance method passed to it
59 | *
60 | * @param {ReactComponent} instance the instance the method is assigned to
61 | * @param {function} method the instance method
62 | * @param {Array} extraArgs additional args to pass to the method
63 | * @returns {function(...Array): any} the method with the instance passed as value
64 | */
65 | export const createMethod = (instance, method, ...extraArgs) => {
66 | if (!isClassComponent(instance)) {
67 | return logInvalidInstanceError('method');
68 | }
69 |
70 | bindMethods(instance);
71 |
72 | const {memoizer} = method;
73 |
74 | delete method.memoizer;
75 |
76 | const fn = (...args) => method.call(instance, instance, args, extraArgs);
77 |
78 | return memoizer ? memoizer(fn) : fn;
79 | };
80 |
81 | /**
82 | * @function createRender
83 | *
84 | * @description
85 | * create a method that is a pure version of the render method
86 | *
87 | * @param {ReactComponent} instance the instance the method is assigned to
88 | * @param {function} render the render method
89 | * @returns {function(): ReactElement} the method with the props and instance passed as values
90 | */
91 | export const createRender = (instance, render) =>
92 | isClassComponent(instance)
93 | ? bindMethods(instance) && ((...args) => render.call(instance, instance.props, instance, args))
94 | : logInvalidInstanceError('render');
95 |
96 | /**
97 | * @function createRenderProps
98 | *
99 | * @description
100 | * create a render props method, where the props passed and the instance it is rendered in are passed as props to it
101 | *
102 | * @param {ReactComponent} instance the instance the method is assigned to
103 | * @param {function} renderProps the render props method
104 | * @returns {function(Object): ReactElement} the method with the props and instance passed as values
105 | */
106 | export const createRenderProps = (instance, renderProps) =>
107 | isClassComponent(instance)
108 | ? bindMethods(instance) && ((props, ...restOfArgs) => renderProps.call(instance, props, instance, restOfArgs))
109 | : logInvalidInstanceError('render props');
110 |
111 | /**
112 | * @function createValue
113 | *
114 | * @description
115 | * create a value to assign to the instance based on props or the instance itself
116 | *
117 | * @param {ReactComponent} instance the instance the method is assigned to
118 | * @param {function} getValue the function to get the value with
119 | * @param {Array} extraArgs additional args to pass to the method
120 | * @returns {function(...Array): any} the method with the instance passed as value
121 | */
122 | export const createValue = (instance, getValue, ...extraArgs) =>
123 | isClassComponent(instance)
124 | ? bindMethods(instance) && getValue.call(instance, instance, extraArgs)
125 | : logInvalidInstanceError('value');
126 |
127 | /**
128 | * @function createComponent
129 | *
130 | * @description
131 | * create a component from the render method and any options passed
132 | *
133 | * @param {function|Object} render the function to render the component, or the options for future curried calls
134 | * @param {Object} [passedOptions] the options to render the component with
135 | * @param {function} [getInitialState] the method to get the initial state with
136 | * @param {boolean} [isPure] is PureComponent used
137 | * @param {function} [onConstruct] a method to call when constructing the component
138 | * @param {Object} [state] the initial state
139 | * @returns {function|ReactComponent} the component class, or a curried call to itself
140 | */
141 | export const createComponent = (render, passedOptions) => {
142 | if (typeof render !== 'function') {
143 | const options = render || {};
144 |
145 | return (render, moreOptions) =>
146 | typeof render === 'function'
147 | ? createComponent(render, {
148 | ...options,
149 | ...(moreOptions || {}),
150 | })
151 | : createComponent({
152 | ...options,
153 | ...(render || {}),
154 | });
155 | }
156 |
157 | const options = passedOptions || {};
158 | const {getInitialState, getInitialValues, isPure, onConstruct, state} = options;
159 |
160 | const Constructor = isPure ? React.PureComponent : React.Component;
161 |
162 | function ParmComponent(initialProps) {
163 | Constructor.call(this, initialProps);
164 |
165 | this.state = typeof getInitialState === 'function' ? createValue(this, getInitialState) : state || null;
166 |
167 | for (let key in options) {
168 | if (!IGNORED_COMPONENT_KEYS[key]) {
169 | const option = options[key];
170 |
171 | this[key] =
172 | typeof option === 'function'
173 | ? option.isRender
174 | ? createRender(this, option)
175 | : option.isRenderProps
176 | ? createRenderProps(this, option)
177 | : createMethod(this, option)
178 | : option;
179 | }
180 | }
181 |
182 | const values = typeof getInitialValues === 'function' ? createValue(this, getInitialValues) : null;
183 |
184 | if (values && typeof values === 'object') {
185 | for (let key in values) {
186 | this[key] = values[key];
187 | }
188 | }
189 |
190 | this.render = createRender(this, render);
191 |
192 | if (typeof onConstruct === 'function') {
193 | onConstruct(this);
194 | }
195 |
196 | return this;
197 | }
198 |
199 | ParmComponent.prototype = Object.create(Constructor.prototype);
200 |
201 | ParmComponent.displayName = render.displayName || render.name || 'ParmComponent';
202 |
203 | Object.keys(render).forEach(
204 | (staticKey) => !IGNORED_STATIC_KEYS[staticKey] && (ParmComponent[staticKey] = render[staticKey])
205 | );
206 |
207 | return ParmComponent;
208 | };
209 |
210 | /**
211 | * @function createPropType
212 | *
213 | * @description
214 | * create a custom prop type handler
215 | *
216 | * @param {function(Object): (Error|null)} handler the prop type handler
217 | * @returns {function} the custom prop type
218 | */
219 | export const createPropType = (handler) =>
220 | addPropTypeIsRequired((props, key, component, locationIgnored, fullKey) =>
221 | handler({
222 | component,
223 | key,
224 | name: fullKey ? fullKey.split(/(\.|\[)/)[0] : key,
225 | path: fullKey || key,
226 | props,
227 | value: props[key],
228 | })
229 | );
230 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-parm
2 |
3 | Handle react classes with more functional purity
4 |
5 | ## Table of contents
6 |
7 | - [Summary](#summary)
8 | - [Usage](#usage)
9 | - [Methods](#methods)
10 | - [createMethod](#createmethod)
11 | - [createValue](#createvalue)
12 | - [createRender](#createrender)
13 | - [createComponent](#createcomponent)
14 | - [createComponentRef](#createcomponentref)
15 | - [createElementRef](#createelementref)
16 | - [createCombinedRef](#createcombinedref)
17 | - [createPropType](#createproptype)
18 | - [Why parm?](#why-parm)
19 | - [Development](#development)
20 |
21 | ## Summary
22 |
23 | `react-parm` is a thin abstraction providing partial-application methods that allow you to handle `react` classes with much more functional purity. This allows for better encapsulation, greater separation of concerns, and simplified testing. When combined with destructuring, it also improves readability and comprehension.
24 |
25 | ## Usage
26 |
27 | ```javascript
28 | import React from "react";
29 | import { createElementRef, createMethod } from "react-parm";
30 |
31 | export const componentDidMount = ({ getFoo, props }) =>
32 | props.shouldGetFoo && getFoo();
33 |
34 | export const onClickGetBar = ({ getBar }, [event]) =>
35 | getBar(event.currentTarget.dataset.baz);
36 |
37 | export default class App extends React.Component {
38 | // lifecycle methods
39 | componentDidMount = createMethod(this, componentDidMount);
40 |
41 | // refs
42 | element = null;
43 |
44 | // instance methods
45 | onClickGetBar = createMethod(this, onClickGetBar);
46 |
47 | render() {
48 | return (
49 |
56 | );
57 | }
58 | }
59 | ```
60 |
61 | ## Methods
62 |
63 | #### createMethod
64 |
65 | Create a functional instance or lifecycle method, which will receive the full instance as the first parameter.
66 |
67 | _createMethod(instance: ReactComponent, method: function, ...extraArgs: Array): (instance: ReactComponent, args: Array, extraArgs: Array) => any_
68 |
69 | ```javascript
70 | import React from "react";
71 | import { createMethod } from "react-parm";
72 |
73 | export const componentDidMount = ({ setState }) =>
74 | setState(() => ({ isMounted: true }));
75 |
76 | export const onClickDoThing = ({ props }, [event], [withStuff]) =>
77 | props.doThing(event.currentTarget, withStuff);
78 |
79 | export default class App extends Component {
80 | state = {
81 | isMounted: false
82 | };
83 |
84 | componentDidMount = createMethod(this, componentDidMount);
85 | onClickDoThing = createMethod(this, onClickDoThing, true);
86 |
87 | render() {
88 | return (
89 |
90 |
Welcome to doing the thing
91 |
92 |
93 |
94 | );
95 | }
96 | }
97 | ```
98 |
99 | If you want this method to be memoized in an instance-specific way, you can assign the function that will memoize the method to the `memoizer` property on the function you create the method from.
100 |
101 | ```javascript
102 | import memoize from "micro-memoize";
103 |
104 | const setCount = ({ setState }, [count]) => setState({ count });
105 |
106 | setCount.memoizer = memoize;
107 | ```
108 |
109 | This will automatically wrap the method you pass to `createMethod` in the `memoizer`.
110 |
111 | #### createValue
112 |
113 | Create a value to assign to the instance based on a functional method which will receive the full instance as the first parameter.
114 |
115 | _createValue(instance: ReactComponent, method: function, ...extraArgs: Array): any_
116 |
117 | ```javascript
118 | import React from "react";
119 | import { createValue } from "react-parm";
120 |
121 | export const getLength = ({ props }) => {
122 | return props.foo.length;
123 | };
124 |
125 | export default class App extends Component {
126 | length = createValue(this, getLength);
127 |
128 | render() {
129 | return
The length of the foo parameter is {this.length}
;
130 | }
131 | }
132 | ```
133 |
134 | #### createRender
135 |
136 | Create a functional render method, which will receive the `props` as the first parameter, the full instance as the second parameter, and any arguments passed to it as the third parameter.
137 |
138 | _createRender(instance: ReactComponent, render: function): (props: Object, instance: ReactComponent, args: Array) => ReactElement_
139 |
140 | ```javascript
141 | import React from "react";
142 | import { createMethod, createRender } from "react-parm";
143 |
144 | export const componentDidMount = ({ setState }) =>
145 | setState(() => ({ isMounted: true }));
146 |
147 | export const DoTheThing = ({ doThing }, { state: { isMounted } }) => {
148 | return (
149 |
150 |
Welcome to doing the mounted thing
151 |
152 | Am I mounted? {isMounted ? "YES!" : "No :("}
153 |
154 |
155 |
156 | );
157 | };
158 |
159 | export default class App extends Component {
160 | state = {
161 | isMounted: false
162 | };
163 |
164 | componentDidMount = createMethod(this, componentDidMount);
165 |
166 | render = createRender(this, DoTheThing);
167 | }
168 | ```
169 |
170 | **NOTE**: The difference in signature from `createMethod` is both for common-use purposes, but also because it allows linting tools to appropriately lint for `PropTypes`.
171 |
172 | #### createRenderProps
173 |
174 | Create a functional [render props method](https://reactjs.org/docs/render-props.html), which will receive the `props` passed to it as the first parameter, the full instance as the second parameter, and any additional arguments passed to it as the third parameter.
175 |
176 | _createRenderProps(instance: ReactComponent, render: function): (props: Object, instance: ReactComponent, remainingArgs: Array) => ReactElement_
177 |
178 | ```javascript
179 | import React from "react";
180 | import { createMethod, createRenderProps } from "react-parm";
181 |
182 | const RenderPropComponent = ({ children }) => (
183 |
192 | );
193 |
194 | export const DoTheThing = ({ doThing }) => (
195 | {renderProps}
196 | );
197 |
198 | export default class App extends Component {
199 | state = {
200 | isMounted: false
201 | };
202 |
203 | renderProps = createRenderProps(this, renderProps);
204 |
205 | render = createRender(this, DoTheThing);
206 | }
207 | ```
208 |
209 | **NOTE**: The main difference between `createRender` and `createRenderProps` is the first `props` argument. In the case of `createRender`, it is the `props` of the `instance` the method is bound to, whereas in the case of `createRenderProps` it is the `props` argument passed to it directly.
210 |
211 | #### createComponent
212 |
213 | Create a functional component with all available instance-based methods, values, and refs a `Component` class has.
214 |
215 | _createComponent(render: function, options: Object): ReactComponent_
216 |
217 | ```javascript
218 | import React from "react";
219 | import { createComponent } from "react-parm";
220 |
221 | export const state = {
222 | isMounted: false
223 | };
224 |
225 | export const componentDidMount = ({ setState }) =>
226 | setState(() => ({ isMounted: true }));
227 |
228 | export const onClickDoThing = ({ props }, [event]) =>
229 | props.doThing(event.currentTarget);
230 |
231 | export const DoTheThing = ({ doThing }, { onClickDoThing }) => (
232 |
233 |
Welcome to doing the thing
234 |
235 |
236 |
237 | );
238 |
239 | DoTheThing.displayName = "DoTheThing";
240 |
241 | DoTheThing.propTypes = {
242 | doThing: PropTypes.func.isRequired
243 | };
244 |
245 | export default createComponent(DoTheThing, {
246 | componentDidMount,
247 | onClickDoThing,
248 | state
249 | });
250 | ```
251 |
252 | **NOTE**: Starting in version `2.6.0`, the `options` can be applied via currying:
253 |
254 | ```javascript
255 | export default createComponent({ componentDidMount, onClickDoThing, state })(
256 | DoTheThing
257 | );
258 | ```
259 |
260 | The component will be parmed with `createRender`, and the properties passed in `options` will be handled as follows:
261 |
262 | - Lifecycle methods will be parmed with `createMethod`
263 | - Instance methods will be parmed with `createMethod`, unless:
264 |
265 | - It has a static property of `isRender` set to `true`, in which case it will be parmed with `createRender`. Example:
266 |
267 | ```javascript
268 | const renderer = ({ foo }) =>
{foo}
;
269 |
270 | renderer.isRender = true;
271 | ```
272 |
273 | - It has a static property of `isRenderProps` set to `true`, in which case it will be parmed with `createRenderProps`. Example:
274 |
275 | ```javascript
276 | const renderProps = ({ children }) =>
{children({child: 'props')}
;
277 |
278 | renderProps.isRenderProps = true;
279 | ```
280 |
281 | - Instance values will be assigned to the instance
282 |
283 | There are also some additional properties that are treated outside the context of assignment to the instance:
284 |
285 | - `getInitialState` => if a method is passed, then it is parmed and used to derive the initial state instead of the static `state` property
286 | - `getInitialValues` => If a method is passed, then it is parmed and used to derive initial instance values
287 | - Expects an object to be returned, where a return of `{foo: 'bar'}` will result in `instance.foo` being `"bar"`
288 | - `isPure` => should `PureComponent` be used to construct the underlying component class instead of `Component` (defaults to `false`)
289 | - `onConstruct` => If a method is passed, then it is called with the instance as parameter at the end of construction
290 |
291 | **NOTE**: Any additional static values / methods you apply to the render component will be re-assigned to the parmed component.
292 |
293 | #### createComponentRef
294 |
295 | Create a method that will assign the Component requested to an instance value using a ref callback.
296 |
297 | _createComponentRef(instance: ReactComponent, ref: string): (component: HTMLElement | ReactComponent) => void_
298 |
299 | ```javascript
300 | import React from "react";
301 | import { createElementRef } from "react-parm";
302 |
303 | export default class App extends Component {
304 | component = null;
305 |
306 | render() {
307 | return (
308 |
309 | We captured the component instance!
310 |
311 | );
312 | }
313 | }
314 | ```
315 |
316 | The `ref` string value passed will be the key that will be used in the assignment to the `instance`.
317 |
318 | #### createElementRef
319 |
320 | Create a method that will assign the DOM node of the component requested to an instance value using a ref callback.
321 |
322 | _createElementRef(instance: ReactComponent, ref: string): (component: HTMLElement | ReactComponent) => void_
323 |
324 | ```javascript
325 | import React from "react";
326 | import { createElementRef } from "react-parm";
327 |
328 | export default class App extends Component {
329 | element = null;
330 |
331 | render() {
332 | return (
333 |
334 | We found the DOM node!
335 |
336 | );
337 | }
338 | }
339 | ```
340 |
341 | The `ref` string value passed will be the key that will be used in the assignment to the `instance`.
342 |
343 | #### createCombinedRef
344 |
345 | Create a method that will assign both the DOM node of the component requested and the component itself to a namespaced instance value using a ref callback.
346 |
347 | _createCombinedRef(instance: ReactComponent, ref: string): (component: HTMLElement | ReactComponent) => void_
348 |
349 | ```javascript
350 | import React from "react";
351 | import { createCombinedRef } from "react-parm";
352 |
353 | export default class App extends Component {
354 | someOtherComponent = null;
355 |
356 | render() {
357 | return (
358 |
359 | I have the best of both worlds! this.someOtherComponent will look like "{component: SomeOtherComponent, element: div}".
360 |
361 | );
362 | }
363 | }
364 | ```
365 |
366 | The value assigned will be an object with `component` and `element` properties, which reflect the component and the DOM node for that component respectively. The `ref` string value passed will be the key that will be used in the assignment to the `instance`.
367 |
368 | #### createPropType
369 |
370 | Create a custom PropTypes validation method.
371 |
372 | _createPropType(validator: function): (metadata: Object) => (Error|null)_
373 |
374 | ```javascript
375 | import { createPropType } from "react-parm";
376 |
377 | export const isFoo = createPropType(({ component, name, value }) =>
378 | value === "foo"
379 | ? null
380 | : new Error(
381 | `The prop "${name}" is "${value}" in ${component}, when it should be "foo"!`
382 | );
383 | );
384 | ```
385 |
386 | The full shape of the `metadata` object passed to `createPropType`:
387 |
388 | ```javascript
389 | {
390 | component: string, // the name of the component
391 | key: string, // the key that is being validated
392 | name: string, // the name of the prop being validated
393 | path: string, // the full path (if nested) of the key being validated
394 | props: any, // the props object
395 | value: any // the value of the prop passed
396 | }
397 | ```
398 |
399 | Please note that usage may result in different values for these keys, based on whether the custom prop type is used in `arrayOf` / `objectOf` or not.
400 |
401 | When used in `arrayOf` or `objectOf`:
402 |
403 | - `key` represents the nested key being validated
404 | - `name` represents the name of the prop that was passed
405 | - `path` represents the full path being validated
406 |
407 | Example:
408 |
409 | ```javascript
410 | const isArrayOfFoo = createPropType(
411 | ({ component, key, name, path, value }) => {
412 | value === "foo"
413 | ? null
414 | : new Error(
415 | `The key "${key}" for prop "${name}" at path ${path} is "${value}" in ${component}, when it should be "foo"!`
416 | );
417 | }
418 | );
419 | ...
420 |
421 | // The key "0" for prop "bar" at path "bar[0]" is "baz" in "SomeComponent", when it should be "foo"!
422 | ```
423 |
424 | When the prop type is used in any context other than `arrayOf` / `objectOf`, then `key`, `name`, and `path` will all be the same value.
425 |
426 | ## Why parm?
427 |
428 | PARM is an acronym, standing for Partial-Application React Method. Also, why not parm? It's delicious.
429 |
430 | ## Development
431 |
432 | Standard stuff, clone the repo and `npm install` dependencies. The npm scripts available:
433 |
434 | - `build` => run rollup to build development and production `dist` files
435 | - `dev` => run webpack dev server to run example app / playground
436 | - `lint` => run ESLint against all files in the `src` folder
437 | - `lint: fix` => runs `lint` with `--fix`
438 | - `prepublish` => runs `prepublish:compile` when publishing
439 | - `prepublish:compile` => run `lint`, `test:coverage`, `transpile:lib`, `transpile:es`, and `build`
440 | - `test` => run AVA test functions with `NODE_ENV=test`
441 | - `test:coverage` => run `test` but with `nyc` for coverage checker
442 | - `test:watch` => run `test`, but with persistent watcher
443 | - `transpile:lib` => run babel against all files in `src` to create files in `lib`
444 | - `transpile:es` => run babel against all files in `src` to create files in `es`, preserving ES2015 modules (for
445 | [`pkg.module`](https://github.com/rollup/rollup/wiki/pkg.module))
446 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | // test
2 | import test from 'ava';
3 | import memoize from 'micro-memoize';
4 | import PropTypes from 'prop-types';
5 | import React from 'react';
6 | import ReactDOM from 'react-dom';
7 | import sinon from 'sinon';
8 |
9 | // src
10 | import * as index from 'src/index';
11 | import * as utils from 'src/utils';
12 |
13 | test('if createCombinedRef will create a ref method that assigns the combined ref to the instance', (t) => {
14 | class OtherValue extends React.Component {
15 | render() {
16 | return ;
17 | }
18 | }
19 |
20 | class Value extends React.Component {
21 | componentDidMount() {
22 | t.true(this.ref.hasOwnProperty('component'));
23 | t.true(this.ref.component instanceof OtherValue);
24 |
25 | t.true(this.ref.hasOwnProperty('element'));
26 | t.true(this.ref.element instanceof HTMLElement);
27 | }
28 |
29 | ref = null;
30 |
31 | render() {
32 | return ;
33 | }
34 | }
35 |
36 | const div = document.createElement('div');
37 |
38 | ReactDOM.render(, div);
39 | });
40 |
41 | test('if createComponent will create a standard component class with static state', (t) => {
42 | const componentDidMount = sinon.spy();
43 |
44 | const state = {
45 | foo: 'quz',
46 | };
47 |
48 | const Generated = function Generated(props, instance) {
49 | t.deepEqual(instance.state, state);
50 |
51 | return
');
336 | });
337 |
338 | test('if createComponent will create a component class with render props methods if they have isRenderProps set to true', (t) => {
339 | const RenderProp = ({children}) =>