├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── package.json └── src └── react-delegate.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | lib 3 | logs 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (http://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # Typescript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | npm-debug.log* 2 | 3 | node_modules/ 4 | .npm 5 | .eslintcache 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Swipes Incorporated 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 | # react-delegate 2 | 3 | Fixing the relationship between parent and child in react to avoid callback hells and over-parenting. :) 4 | 5 | # Installation 6 | ``` 7 | npm install --save react-delegate 8 | ``` 9 | 10 | # Basic idea (Parent) 11 | The delegate turns a TaskList component (parent): 12 | ```jsx 13 | /* BEFORE */ 14 | class TaskList extends React.Component { 15 | constructor(props) { 16 | super(props); 17 | this.onTaskComplete = this.onTaskComplete.bind(this); 18 | this.onTaskDelete = this.onTaskDelete.bind(this); 19 | this.onTaskSchedule = this.onTaskSchedule.bind(this); 20 | } 21 | onTaskComplete() { /* completing stuff */ } 22 | onTaskDelete() { /* deleting stuff */ } 23 | onTaskSchedule() { /* scheduling stuff */ } 24 | 25 | render() { 26 | const { tasks } = this.props; 27 | 28 | return tasks.map(task => ( 29 | 35 | )); 36 | } 37 | } 38 | ``` 39 | 40 | Into looking like that (parent): 41 | ```jsx 42 | /* AFTER */ 43 | class TaskList extends React.Component { 44 | constructor(props) { 45 | super(props); 46 | } 47 | onTaskComplete() { /* completing stuff */ } 48 | onTaskDelete() { /* deleting stuff */ } 49 | onTaskSchedule() { /* scheduling stuff */ } 50 | 51 | render() { 52 | const { tasks } = this.props; 53 | 54 | return tasks.map(task => ( 55 | 59 | )); 60 | } 61 | } 62 | ``` 63 | 64 | # Simple setup (child) 65 | And it is super easy to set up in the Task component (child) 66 | ```jsx 67 | import { setupDelegate } from 'react-delegate'; 68 | class Task extends React.Component { 69 | constructor(props) { 70 | super(props); 71 | setupDelegate(this, 'onTaskComplete', 'onTaskDelete', 'onTaskSchedule'); 72 | } 73 | 74 | render() { 75 | const { task } = this.props; 76 | return ( 77 |
78 | {task.title} 79 |
80 | ) 81 | } 82 | } 83 | ``` 84 | 85 | OBS: if the delegate does not implement a function nothing will happen and we will ignore the call. 86 | 87 | # Advanced setup (child) 88 | 89 | Sometimes you need to send properties back to the parent like taskId or the task itself. 90 | 91 | TaskList wants to receive arguments (parent) 92 | ```jsx 93 | class TaskList extends React.Component { 94 | ... 95 | onTaskComplete(taskId, task, e) { 96 | // here you get the arguments from below 97 | } 98 | ... 99 | } 100 | ``` 101 | This can be achieved in two ways. 102 | ## 1. setGlobals (child) 103 | You can call setGlobals and prepend arguments to all the delegate calls. 104 | This is useful in a class like Task that want to send which task is being completed, deleted or scheduled. 105 | ```jsx 106 | import { setupDelegate } from 'react-delegate'; 107 | class Task extends React.Component { 108 | constructor(props) { 109 | super(props); 110 | setupDelegate(this, 'onTaskComplete', 'onTaskDelete', 'onTaskSchedule').setGlobals(task.id, task); 111 | // Now all calls to these methods will send task.id and task as the first two arguments. 112 | } 113 | 114 | render() { 115 | const { task } = this.props; 116 | return ( 117 |
118 | {task.title} 119 |
120 | ) 121 | } 122 | } 123 | ``` 124 | 125 | ## 2. ...Cached version (child) 126 | Each method you setup gets a cached version as well that you can pass parameters to. 127 | This is great if you need to reuse a callback for multiple purposes or multiple rows. 128 | ```jsx 129 | import { setupDelegate } from 'react-delegate'; 130 | class Task extends React.Component { 131 | constructor(props) { 132 | super(props); 133 | setupDelegate(this, 'onTaskComplete', 'onTaskDelete', 'onTaskSchedule'); 134 | // This sets up a this.onTaskCompleteCached that take arguments. 135 | } 136 | 137 | render() { 138 | const { task } = this.props; 139 | return ( 140 |
141 | {task.title} 142 |
143 | ) 144 | } 145 | } 146 | ``` 147 | The arguments will be added before any standard arguments like the event (e). 148 | 149 | OBS: The first argument to the cached methods acts as a key. This is to avoid creating a new function all the time on render. So make sure that if you render a list of tasks etc, to use a unique key as the first argument. 150 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-delegate", 3 | "version": "1.0.2", 4 | "description": "React delegate pattern for better callbacks to fix the parent/child relationship", 5 | "main": "./lib/react-delegate.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/swipesapp/react-delegate.git" 9 | }, 10 | "keywords": [ 11 | "react", 12 | "delegate", 13 | "callback" 14 | ], 15 | "author": "Kasper Pihl Tornoe", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/swipesapp/react-delegate/issues" 19 | }, 20 | "homepage": "https://github.com/swipesapp/react-delegate#readme", 21 | "devDependencies": { 22 | "babel-cli": "^6.24.1", 23 | "babel-plugin-add-module-exports": "^0.2.1", 24 | "babel-plugin-transform-object-rest-spread": "^6.23.0", 25 | "babel-preset-es2015": "^6.24.1" 26 | }, 27 | "peerDependencies": { 28 | "react": ">=0.15" 29 | }, 30 | "babel": { 31 | "presets": [ 32 | "es2015" 33 | ], 34 | "plugins": [ 35 | "transform-object-rest-spread", 36 | "add-module-exports" 37 | ] 38 | }, 39 | "scripts": { 40 | "compile": "babel -d lib/ src/", 41 | "prepublish": "npm run compile" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/react-delegate.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export function setupCachedCallback(method, ctx) { 4 | const cachedMethod = {}; 5 | return function cachedCallback(id) { 6 | id = id || ''; 7 | if(typeof(id) === 'object') { 8 | id = JSON.stringify(id); 9 | } 10 | if (!cachedMethod[id]) { 11 | const args = Array.from(arguments); 12 | cachedMethod[id] = method.bind(ctx, ...args); 13 | } 14 | return cachedMethod[id]; 15 | }; 16 | } 17 | 18 | export function setupDelegate(obj, ...delegateMethods) { 19 | let globals = []; 20 | 21 | const delegate = obj && (obj.delegate || (obj.props && obj.props.delegate)); 22 | obj.callDelegate = function callDelegate(name, ...rest) { 23 | if (delegate && typeof delegate[name] === 'function') { 24 | return delegate[name](...globals.concat(rest)); 25 | } 26 | 27 | return undefined; 28 | }; 29 | 30 | delegateMethods.forEach((funcName) => { 31 | if(typeof funcName === 'string'){ 32 | obj[funcName] = obj.callDelegate.bind(null, funcName); 33 | obj[`${funcName}Cached`] = setupCachedCallback(obj[funcName]); 34 | } 35 | }) 36 | 37 | return { 38 | setGlobals: (...globalArgs) => { 39 | globals = globals.concat(globalArgs); 40 | } 41 | } 42 | } 43 | 44 | export function withDelegate(methods) { 45 | return (component) => { 46 | class withDelegate extends React.Component { 47 | constructor(props) { 48 | super(props); 49 | this.methods = { delegate: props.delegate }; 50 | setupDelegate(this.methods, ...methods); 51 | } 52 | render() { 53 | const { delegate, ...otherProps } = this.props; 54 | const { delegate: d, ...allMethods } = this.methods; 55 | 56 | return React.createElement(component, { ...otherProps, ...allMethods }); 57 | } 58 | } 59 | 60 | return withDelegate; 61 | } 62 | } 63 | --------------------------------------------------------------------------------