├── .babelrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── __snapshots__ └── test.js.snap ├── examples ├── App.js ├── Counter.js ├── Demo.js ├── List.js ├── README.md ├── package.json └── updaters.js ├── package.json ├── src └── index.js └── test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env", 4 | "stage-0", 5 | "react" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | package-lock.json 4 | dist 5 | .nyc_output 6 | coverage 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | docs 3 | examples 4 | .nyc_output 5 | coverage 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 8 4 | after_success: 5 | - npm run cover 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | # The MIT License (MIT) 3 | Copyright (c) 2017 Brent Jackson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Refunk 🎧 3 | 4 | Simple React functional setState 5 | with the new [React context API][context] (requires React v16.3 or later) 6 | 7 | 8 | ```sh 9 | npm i refunk 10 | ``` 11 | 12 | ## Getting Started 13 | 14 | ```jsx 15 | import React from 'react' 16 | import { connect } from 'refunk' 17 | 18 | // Create a state provider component 19 | const App = connect(props => ( 20 |
21 |

count: {props.count}

22 | 23 |
24 | )) 25 | 26 | // Updaters are functions that return state 27 | const dec = state => ({ count: state.count - 1 }) 28 | const inc = state => ({ count: state.count + 1 }) 29 | 30 | // Connect the Controls component to the App state 31 | const Controls = connect(props => ( 32 |
33 | {props.count} 34 | 37 | 40 |
41 | )) 42 | 43 | const initialState = { 44 | count: 0 45 | } 46 | 47 | // initialize state with props 48 | render() 49 | ``` 50 | 51 | ## Usage 52 | 53 | Refunk components initialize state from props and provide an `update` function to their consumers. 54 | When nesting Refunk components, the top-most component will control state for any child Refunk components. 55 | 56 | The `update` function works the same as `setState`, but it's intended to be used with separate [updater functions](#using-updaters), 57 | that can be shared across many parts of an application. 58 | 59 | ### connect 60 | 61 | The `connect` higher-order component creates state based on props for top-level components or connects into a parent Refunk component's state when nested. 62 | This allows for the creation of stateful components that can work standalone or listen to a parent's state. 63 | 64 | ```jsx 65 | import React from 'react' 66 | import { connect } from 'refunk' 67 | 68 | const App = connect(props => ( 69 |
70 | {props.count} 71 |
72 | )) 73 | 74 | App.defaultProps = { 75 | count: 0 76 | } 77 | 78 | export default App 79 | ``` 80 | 81 | ### Provider 82 | 83 | For lower-level access to React's context API, the Provider component can be used to create a context. 84 | The Refunk Provider will convert props to initial state and provide the state and `update` function through context. 85 | 86 | ```jsx 87 | import React from 'react' 88 | import { Provider } from 'refunk' 89 | 90 | const App = props => ( 91 | 92 |
93 | 94 | ) 95 | ``` 96 | 97 | ### Consumer 98 | 99 | The context Consumer is also exported for lower-level access to the context API. 100 | 101 | ```jsx 102 | import React from 'react' 103 | import { Provider, Consumer } from 'refunk' 104 | 105 | const inc = state => ({ count: state.count + 1 }) 106 | 107 | const App = props => ( 108 | 109 | 110 | {state => ( 111 | 112 | {state.count} 113 | 114 | 115 | )} 116 | 117 | 118 | ) 119 | ``` 120 | 121 | ### Using Updaters 122 | 123 | Updaters are functions that are passed to the `props.update()` function. 124 | An updater function takes `state` as its only argument and returns a new state. 125 | 126 | ```jsx 127 | // updaters.js 128 | // Create an `updaters` module with functions to update the state of the app 129 | export const decrement = state => ({ count: state.count - 1 }) 130 | export const increment = state => ({ count: state.count + 1 }) 131 | ``` 132 | 133 | ```jsx 134 | // Counter.js 135 | // Use the updater functions in the connected Counter component 136 | import React from 'react' 137 | import { connect } from 'refunk' 138 | import { decrement, increment } from './updaters' 139 | 140 | const Counter = props => ( 141 |
142 | Count: {props.count} 143 | 146 | 149 |
150 | ) 151 | 152 | export default connect(Counter) 153 | ``` 154 | 155 | ```jsx 156 | // App.js 157 | // Include the Counter component in App 158 | import React from 'react' 159 | import { connect } from 'refunk' 160 | import Counter from './Counter' 161 | 162 | const App = props => ( 163 |
164 |

Hello

165 | 166 |
167 | ) 168 | 169 | export default connect(App) 170 | ``` 171 | 172 | ## Build Your Own 173 | 174 | Refunk's [source](src) is only about 50 LOC and relies on built-in React functionality. 175 | This library is intended to be used directly as a package and also to serve as an example of some ways to handle state in a React application. 176 | Feel free to fork or steal ideas from this project, and build your own version. 177 | 178 | 179 | ## Concepts 180 | 181 | Refunk is meant as a simpler, smaller alternative to other state 182 | managment libraries that makes use of React's built-in component state. 183 | Refunk uses higher-order components, the new [context API][context], and React component state management along with 184 | [functional setState][setState] 185 | to help promote the separation of presentational and container components, 186 | and to keep state updating logic outside of the components themselves. 187 | 188 | This library also promotes keeping application state in a single location, 189 | similar to other [Flux][flux] libraries and [Redux][redux]. 190 | 191 | 192 | ### Related 193 | 194 | - [microstate](https://github.com/estrattonbailey/microstate) 195 | - [statty](https://github.com/vesparny/statty) 196 | - [unistore](https://github.com/developit/unistore) 197 | - [redux][redux] 198 | - [unstated](https://github.com/jamiebuilds/unstated) 199 | 200 | [context]: https://reactjs.org/docs/context.html 201 | [setState]: https://facebook.github.io/react/docs/react-component.html#setstate 202 | [flux]: http://facebook.github.io/flux/ 203 | [redux]: http://redux.js.org/ 204 | 205 | --- 206 | 207 | [Made by Jxnblk](http://jxnblk.com) | [MIT License](LICENSE.md) 208 | -------------------------------------------------------------------------------- /__snapshots__/test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Component renders 1`] = ` 4 |
5 | Hello 6 |
7 | `; 8 | -------------------------------------------------------------------------------- /examples/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from '..' 3 | import Counter from './Counter' 4 | import List from './List' 5 | 6 | const App = connect(props => ( 7 |
11 |

refunk {props.count}

12 | 13 | 14 |
15 | )) 16 | 17 | App.defaultProps = { 18 | items: [], 19 | newItem: '', 20 | count: 0 21 | } 22 | 23 | export default App 24 | -------------------------------------------------------------------------------- /examples/Counter.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from '..' 3 | import { dec, inc } from './updaters' 4 | 5 | const Counter = props => ( 6 |
7 |

Count: {props.count}

8 |
11 | ) 12 | 13 | export default connect(Counter) 14 | -------------------------------------------------------------------------------- /examples/Demo.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | Provider, 4 | Consumer, 5 | connect 6 | } from '../src' 7 | 8 | const Box = props =>
9 | 10 | const App = connect(props => ( 11 | 12 | 13 |

Refunk

14 | {props.count} 15 | 44 |
45 | )) 46 | 47 | const dec = state => ({ count: state.count - 1 }) 48 | const inc = state => ({ count: state.count + 1 }) 49 | 50 | App.defaultProps = { 51 | count: 0 52 | } 53 | 54 | export default App 55 | -------------------------------------------------------------------------------- /examples/List.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from '..' 3 | import { 4 | removeItem, 5 | addItem, 6 | setNewItem 7 | } from './updaters' 8 | 9 | const List = props => ( 10 |
11 |

{props.items.length} Items

12 |
    13 | {props.items.map((item, i) => ( 14 |
  • 15 | {item} 16 |
  • 21 | ))} 22 |
23 |
{ 24 | e.preventDefault() 25 | props.update(addItem) 26 | }}> 27 | 28 | props.update(setNewItem(e.target.value))} 34 | /> 35 |
38 | ) 39 | 40 | export default connect(List) 41 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Examples 3 | 4 | Install dependencies 5 | 6 | ```sh 7 | npm install 8 | ``` 9 | 10 | Run the example in development mode 11 | 12 | ```sh 13 | npm start 14 | ``` 15 | 16 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "refunk-examples", 3 | "private": true, 4 | "scripts": { 5 | "start": "ok App.js -o" 6 | }, 7 | "devDependencies": { 8 | "ok-cli": "^1.0.0-11" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/updaters.js: -------------------------------------------------------------------------------- 1 | 2 | export const dec = state => ({ count: state.count - 1 }) 3 | export const inc = state => ({ count: state.count + 1 }) 4 | 5 | export const setNewItem = value => state => ({ newItem: value }) 6 | export const addItem = state => ({ 7 | newItem: '', 8 | items: [ 9 | ...state.items, 10 | state.newItem 11 | ] 12 | }) 13 | export const removeItem = i => state => ({ 14 | items: [ 15 | ...state.items.slice(0, i), 16 | ...state.items.slice(i + 1) 17 | ] 18 | }) 19 | 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "refunk", 3 | "version": "3.0.1", 4 | "description": "Simple functional setState for React", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "prepublish": "babel src -d dist", 8 | "size": "bundlesize", 9 | "cover": "nyc report --reporter=html", 10 | "test": "nyc ava" 11 | }, 12 | "devDependencies": { 13 | "ava": "^0.19.1", 14 | "babel-cli": "^6.24.1", 15 | "babel-core": "^6.24.1", 16 | "babel-preset-env": "^1.6.0", 17 | "babel-preset-react": "^6.24.1", 18 | "babel-preset-stage-0": "^6.24.1", 19 | "babel-register": "^6.24.1", 20 | "bundlesize": "^0.17.0", 21 | "nyc": "^11.2.1", 22 | "react": "^16.3.0", 23 | "react-test-renderer": "^16.3.0" 24 | }, 25 | "dependencies": { 26 | "prop-types": "^15.5.10" 27 | }, 28 | "keywords": [ 29 | "react", 30 | "state", 31 | "functional", 32 | "context", 33 | "render-props", 34 | "hoc" 35 | ], 36 | "author": "Brent Jackson", 37 | "license": "MIT", 38 | "repository": { 39 | "type": "git", 40 | "url": "git+https://github.com/jxnblk/refunk.git" 41 | }, 42 | "bugs": { 43 | "url": "https://github.com/jxnblk/refunk/issues" 44 | }, 45 | "homepage": "https://github.com/jxnblk/refunk#readme", 46 | "ava": { 47 | "require": [ 48 | "babel-register" 49 | ], 50 | "babel": "inherit" 51 | }, 52 | "bundlesize": [ 53 | { 54 | "path": "src/index.js", 55 | "maxSize": "0.5 kB" 56 | }, 57 | { 58 | "path": "dist/index.js", 59 | "maxSize": "1.5 kB" 60 | } 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Context = React.createContext(null) 4 | 5 | export const Consumer = props => 6 | 7 | const omit = (obj, keys) => { 8 | const next = {} 9 | for (let key in obj) { 10 | if (keys.indexOf(key) > -1) continue 11 | next[key] = obj[key] 12 | } 13 | return next 14 | } 15 | 16 | export class Provider extends React.Component { 17 | state = omit(this.props, 'children') 18 | 19 | update = (...args) => this.setState(...args) 20 | 21 | render () { 22 | const value = { 23 | ...this.state, 24 | update: this.update 25 | } 26 | 27 | return ( 28 | 29 | {this.props.children} 30 | 31 | ) 32 | } 33 | } 34 | 35 | export const connect = Component => props => ( 36 | 37 | {maybeState => maybeState ? ( 38 | 39 | ) : ( 40 | 41 | 42 | {state => ( 43 | 44 | )} 45 | 46 | 47 | )} 48 | 49 | ) 50 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import React from 'react' 3 | import { create as render } from 'react-test-renderer' 4 | import ShallowRenderer from 'react-test-renderer/shallow' 5 | import { 6 | connect, 7 | Provider, 8 | Consumer 9 | } from './src' 10 | 11 | test('exports a connect function', t => { 12 | t.is(typeof connect, 'function') 13 | }) 14 | 15 | test('exports Provider component', t => { 16 | t.is(typeof Provider, 'function') 17 | }) 18 | 19 | test('exports Consumer component', t => { 20 | t.is(typeof Consumer, 'function') 21 | }) 22 | 23 | test('Provider derives state from props', t => { 24 | const root = render().root 25 | t.is(root.instance.state.count, 1) 26 | }) 27 | 28 | test('Provider.update() sets state', t => { 29 | const root = render().root 30 | const instance = root.instance 31 | instance.update(state => ({ count: 2 })) 32 | t.is(instance.state.count, 2) 33 | }) 34 | 35 | test('Nested components get state from parent', t => { 36 | const Nested = connect(props =>
{props.count}
) 37 | const json = render( 38 | 39 | 40 | 41 | ).toJSON() 42 | t.is(json.children[0], '1') 43 | }) 44 | 45 | test('Unnested components create their own Provider', t => { 46 | const Unnested = connect(props =>
{props.count}
) 47 | const json = render( 48 | 49 | ).toJSON() 50 | t.is(json.children[0], '3') 51 | }) 52 | 53 | --------------------------------------------------------------------------------