├── .gitignore
├── .travis.yml
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── demo
└── src
│ ├── components
│ ├── Calculator.js
│ ├── Counter.js
│ ├── FriendsList.js
│ ├── Items.js
│ ├── Notifications.js
│ ├── PhotosList.js
│ └── Row.js
│ ├── index.js
│ ├── organisms
│ ├── Calculator.js
│ ├── Counter.js
│ ├── Counter2.js
│ ├── Counter3.js
│ ├── Counter4.js
│ ├── Items.js
│ ├── ItemsChoice.js
│ └── Social.js
│ └── state
│ ├── counter.js
│ ├── friends.js
│ ├── photos.js
│ ├── placeholderAPI.js
│ └── selection.js
├── nwb.config.js
├── package.json
├── packages
└── create-react-organism
│ ├── .gitignore
│ ├── README.md
│ ├── bin
│ └── create-react-organism.js
│ ├── package.json
│ └── yarn.lock
├── src
├── adjustArgs
│ └── extractFromDOM.js
├── index.d.ts
├── index.js
├── multi.js
└── nextFrame.js
├── tests
├── .eslintrc
├── extractFromDOM-test.js
├── index-test.js
└── multi-test.js
├── umd
├── react-organism.js
├── react-organism.min.js
└── react-organism.min.js.map
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | /coverage
2 | /demo/dist
3 | /es
4 | /lib
5 | /node_modules
6 | npm-debug.log*
7 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 |
3 | language: node_js
4 | node_js:
5 | - 4
6 | - 6
7 | - 7
8 | - 8
9 |
10 | before_install:
11 | - npm install codecov.io coveralls
12 |
13 | after_success:
14 | - cat ./coverage/lcov.info | ./node_modules/codecov.io/bin/codecov.io.js
15 | - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js
16 |
17 | branches:
18 | only:
19 | - master
20 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## Prerequisites
2 |
3 | [Node.js](http://nodejs.org/) >= v4 must be installed.
4 |
5 | ## Installation
6 |
7 | - Running `npm install` in the components's root directory will install everything you need for development.
8 |
9 | ## Demo Development Server
10 |
11 | - `npm start` will run a development server with the component's demo app at [http://localhost:3000](http://localhost:3000) with hot module reloading.
12 |
13 | ## Running Tests
14 |
15 | - `npm test` will run the tests once.
16 |
17 | - `npm run test:coverage` will run the tests and produce a coverage report in `coverage/`.
18 |
19 | - `npm run test:watch` will run the tests on every change.
20 |
21 | ## Building
22 |
23 | - `npm run build` will build the component for publishing to npm and also bundle the demo app.
24 |
25 | - `npm run clean` will delete built resources.
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Patrick Smith
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 Organism
2 |
3 | [![Travis][build-badge]][build]
4 | [![npm package][npm-badge]][npm]
5 | [![Coveralls][coveralls-badge]][coveralls]
6 |
7 | **Dead simple React/Preact state management to bring pure components alive**
8 |
9 | - Supports `async`/`await` and easy loading (e.g. `fetch()`)
10 | - Reload when particular props change
11 | - Animate using generator functions: just `yield` the new state for each frame
12 | - Tiny: 1.69 KB gzipped (3.49 KB uncompressed)
13 | - Embraces the existing functional `setState` while avoiding boilerplate (no writing `this.setState()` or `.bind` again)
14 | - Easy to unit test
15 |
16 | #### Table of contents
17 |
18 | - [Installation](#installation)
19 | - [Demos](#demos)
20 | - [Usage](#usage)
21 | - [Basic](#basic)
22 | - [Using props](#using-props)
23 | - [Async & promises](#async)
24 | - [Handling events](#handling-events)
25 | - [Animation](#animation)
26 | - [Serialization: Local storage](#serialization-local-storage)
27 | - [Separate and reuse state handlers](#separate-and-reuse-state-handlers)
28 | - [Multicelled organisms](#multicelled-organisms)
29 | - [API](#api)
30 | - [`makeOrganism(PureComponent, StateFunctions, options)`](#makeorganismpurecomponent-statefunctions-options)
31 | - [State functions](#state-functions)
32 | - [Argument enhancers](#argument-enhancers)
33 | - [Why instead of Redux?](#why-instead-of-redux)
34 |
35 | ## Installation
36 |
37 | ```
38 | npm i react-organism --save
39 | ```
40 |
41 | ## Demos
42 |
43 | - [Animated counter](https://codesandbox.io/s/2vx12v3qmn)
44 | - [Dynamic loading with `import()`](https://codesandbox.io/s/X6mLEwG7W)
45 | - [Live form error validation with Yup](https://codesandbox.io/s/4xQpKRRWx)
46 | - [Multicelled component — using multiple states](https://codesandbox.io/s/Yv7j1xLqM)
47 | - [Todo List](https://codesandbox.io/s/yME5Y3Yz)
48 | - [Inputs, forms, animation, fetch](https://react-organism.now.sh) · [code](https://github.com/BurntCaramel/react-organism/tree/master/demo/src)
49 | - [User Stories Maker](https://codesandbox.io/s/xkZ5ZONl)
50 | - [React Cheat Sheet](https://react-cheat.now.sh/) · [code](https://github.com/BurntCaramel/react-cheat)
51 |
52 | ## Usage
53 |
54 | ### Basic
55 |
56 | ```js
57 | // organisms/Counter.js
58 | import makeOrganism from 'react-organism'
59 | import Counter from './components/Counter'
60 |
61 | export default makeOrganism(Counter, {
62 | initial: () => ({ count: 0 }),
63 | increment: () => ({ count }) => ({ count: count + 1 }),
64 | decrement: () => ({ count }) => ({ count: count - 1 })
65 | })
66 | ```
67 |
68 | ```js
69 | // components/Counter.js
70 | import React, { Component } from 'react'
71 |
72 | export default function Counter({
73 | count,
74 | handlers: {
75 | increment,
76 | decrement
77 | }
78 | }) {
79 | return (
80 |
81 |
82 | { count }
83 |
84 |
85 | )
86 | }
87 | ```
88 |
89 | ### Using props
90 |
91 | The handlers can easily use props, which are always passed as the first argument
92 |
93 | ```js
94 | // organisms/Counter.js
95 | import makeOrganism from 'react-organism'
96 | import Counter from './components/Counter'
97 |
98 | export default makeOrganism(Counter, {
99 | initial: ({ initialCount = 0 }) => ({ count: initialCount }),
100 | increment: ({ stride = 1 }) => ({ count }) => ({ count: count + stride }),
101 | decrement: ({ stride = 1 }) => ({ count }) => ({ count: count - stride })
102 | })
103 |
104 | // Render passing prop:
105 | ```
106 |
107 | ### Async
108 |
109 | Asynchronous code to load from an API is easy:
110 |
111 | ```js
112 | // components/Items.js
113 | import React, { Component } from 'react'
114 |
115 | export default function Items({
116 | items,
117 | collectionName,
118 | handlers: {
119 | load
120 | }
121 | }) {
122 | return (
123 |
124 | {
125 | !!items ? (
126 | `${items.length} ${collectionName}`
127 | ) : (
128 | 'Loading…'
129 | )
130 | }
131 |
132 |
133 |
134 |
135 | )
136 | }
137 | ```
138 |
139 | ```js
140 | // organisms/Items.js
141 | import makeOrganism from 'react-organism'
142 | import Items from '../components/Items'
143 |
144 | const baseURL = 'https://jsonplaceholder.typicode.com'
145 | const fetchAPI = (path) => fetch(baseURL + path).then(r => r.json())
146 |
147 | export default makeOrganism(Items, {
148 | initial: () => ({ items: null }),
149 |
150 | load: async ({ path }, prevProps) => {
151 | if (!prevProps || path !== prevProps.path) {
152 | return { items: await fetchAPI(path) }
153 | }
154 | }
155 | })
156 | ```
157 |
158 | ```js
159 |
160 |
161 |
162 |
163 | ```
164 |
165 | ### Handling events
166 |
167 | Handlers can easily accept arguments such as events.
168 |
169 | ```js
170 | // components/Calculator.js
171 | import React, { Component } from 'react'
172 |
173 | export default function Calculator({
174 | value,
175 | handlers: {
176 | changeValue,
177 | double,
178 | add3,
179 | initial
180 | }
181 | }) {
182 | return (
183 |
184 |
185 |
186 |
187 |
188 |
189 | )
190 | }
191 | ```
192 |
193 | ```js
194 | // organisms/Calculator.js
195 | import makeOrganism from 'react-organism'
196 | import Calculator from '../components/Calculator'
197 |
198 | export default makeOrganism(Calculator, {
199 | initial: ({ initialValue = 0 }) => ({ value: initialValue }),
200 | // Destructure event to get target
201 | changeValue: (props, { target }) => ({ value }) => ({ value: parseInt(target.value, 10) }),
202 | double: () => ({ value }) => ({ value: value * 2 }),
203 | add3: () => ({ value }) => ({ value: value + 3 })
204 | })
205 | ```
206 |
207 | ### Animation
208 |
209 | ```js
210 | import makeOrganism from 'react-organism'
211 | import Counter from '../components/Counter'
212 |
213 | export default makeOrganism(Counter, {
214 | initial: ({ initialCount = 0 }) => ({ count: initialCount }),
215 | increment: function * ({ stride = 20 }) {
216 | while (stride > 0) {
217 | yield ({ count }) => ({ count: count + 1 })
218 | stride -= 1
219 | }
220 | },
221 | decrement: function * ({ stride = 20 }) {
222 | while (stride > 0) {
223 | yield ({ count }) => ({ count: count - 1 })
224 | stride -= 1
225 | }
226 | }
227 | })
228 | ```
229 |
230 | ### Automatically extract from `data-` attributes and ``
231 |
232 | Example coming soon
233 |
234 | ### Serialization: Local storage
235 |
236 | ```js
237 | // organisms/Counter.js
238 | import makeOrganism from 'react-organism'
239 | import Counter from '../components/Counter'
240 |
241 | const localStorageKey = 'counter'
242 |
243 | export default makeOrganism(Counter, {
244 | initial: ({ initialCount = 0 }) => ({ count: initialCount }),
245 | load: async (props, prevProps) => {
246 | if (!prevProps) {
247 | // Try commenting out:
248 | /* throw (new Error('Oops!')) */
249 |
250 | // Load previously stored state, if present
251 | return await JSON.parse(localStorage.getItem(localStorageKey))
252 | }
253 | },
254 | increment: ({ stride = 1 }) => ({ count }) => ({ count: count + stride }),
255 | decrement: ({ stride = 1 }) => ({ count }) => ({ count: count - stride })
256 | }, {
257 | onChange(state) {
258 | // When state changes, save in local storage
259 | localStorage.setItem(localStorageKey, JSON.stringify(state))
260 | }
261 | })
262 | ```
263 |
264 | ### Separate and reuse state handlers
265 |
266 | React Organism supports separating state handlers and the component into their own files. This means state handlers could be reused by multiple smart components.
267 |
268 | Here’s an example of separating state:
269 |
270 | ```js
271 | // state/counter.js
272 | export const initial = () => ({
273 | count: 0
274 | })
275 |
276 | export const increment = () => ({ count }) => ({ count: count + 1 })
277 | export const decrement = () => ({ count }) => ({ count: count - 1 })
278 | ```
279 |
280 | ```js
281 | // organisms/Counter.js
282 | import makeOrganism from 'react-organism'
283 | import Counter from './components/Counter'
284 | import * as counterState from './state/counter'
285 |
286 | export default makeOrganism(Counter, counterState)
287 | ```
288 |
289 | ```js
290 | // App.js
291 | import React from 'react'
292 | import CounterOrganism from './organisms/Counter'
293 |
294 | class App extends React.Component {
295 | render() {
296 | return (
297 |
298 |
299 |
300 | )
301 | }
302 | }
303 | ```
304 |
305 | ### Multicelled Organisms
306 |
307 | Example coming soon.
308 |
309 |
310 | ## API
311 |
312 | ### `makeOrganism(PureComponent, StateFunctions, options?)`
313 | ```js
314 | import makeOrganism from 'react-organism'
315 | ```
316 | Creates a smart component, rendering using React component `PureComponent`, and managing state using `StateFunctions`.
317 |
318 | #### `PureComponent`
319 | A React component, usually a pure functional component. This component is passed as its props:
320 |
321 | - The props passed to the smart component, combined with
322 | - The current state, combined with
323 | - `handlers` which correspond to each function in `StateFunctions` and are ready to be passed to e.g. `onClick`, `onChange`, etc.
324 | - `loadError?`: Error produced by the `load` handler
325 | - `handlerError?`: Error produced by any other handler
326 |
327 | #### `StateFunctions`
328 | Object with functional handlers. See [state functions below](#state-functions).
329 |
330 | Either pass a object directly with each function, or create a separate file with each handler function `export`ed out, and then bring in using `import * as StateFunctions from '...'`.
331 |
332 | #### `options`
333 |
334 | ##### `adjustArgs?(args: array) => newArgs: array`
335 |
336 | Used to enhance handlers. See [built-in handlers below](#argument-enhancers).
337 |
338 | ##### `onChange?(state)`
339 |
340 | Called after the state has changed, making it ideal for saving the state somewhere (e.g. Local Storage).
341 |
342 |
343 | ### State functions
344 |
345 | Your state is handled by a collection of functions. Each function is pure: they can only rely on the props and state passed to them. Functions return the new state, either immediately or asynchronously.
346 |
347 | Each handler is passed the current props first, followed by the called arguments:
348 | - `(props, event)`: most event handlers, e.g. `onClick`, `onChange`
349 | - `(props, first, second)`: e.g. `handler(first, second)`
350 | - `(props, ...args)`: get all arguments passed
351 | - `(props)`: ignore any arguments
352 | - `()`: ignore props and arguments
353 |
354 | Handlers must return one of the following:
355 | - An object with new state changes, a la React’s `setState(changes)`.
356 | - A function accepting the previous state and current props, and returns the new state, a la React’s `setState((prevState, props) => changes)`.
357 | - A promise resolving to any of the above (object / function), which will then be used to update the state. Uncaught errors are stored in state under the key `handlerError`. Alternatively, your handler can use the `async`/`await` syntax.
358 | - An iterator, such as one made by using a [generator function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function%2A). Each object passed to `yield` may be one of the above (object / function / promise).
359 | - An array of any of the above (object / function / promise / iterator).
360 | - Or optionally, nothing.
361 |
362 | There are some handlers for special tasks, specifically:
363 |
364 | #### `initial(props) => object` (required)
365 | Return initial state to start off with, a la React’s `initialState`. Passed props.
366 |
367 | #### `load(props: object, prevProps: object?, { handlers: object }) => object | Promise | void` (optional)
368 | Passed the current props and the previous props. Return new state, a Promise returning new state, or nothing. You may also use a generator function (`function * load(props, prevProps)`) and `yield` state changes.
369 |
370 | If this is the first time loaded or if being reloaded, then `prevProps` is `null`.
371 |
372 | Usual pattern is to check for either `prevProps` being `null` or if the prop of interest has changed from its previous value:
373 | ```js
374 | export const load = async ({ id }, prevProps) => {
375 | if (!prevProps || id !== prevProps.id) {
376 | return { item: await loadItem(id) }
377 | }
378 | }
379 | ```
380 |
381 | Your `load` handler will be called in React’s lifecycle: `componentDidMount` and `componentWillReceiveProps`.
382 |
383 |
384 | ### Argument enhancers
385 |
386 | Handler arguments can be adjusted, to cover many common cases. Pass them to the `adjustArgs` option. The following enhancers are built-in:
387 |
388 | #### `extractFromDOM(args: array) => newArgs: array`
389 | ```js
390 | import extractFromDOM from 'react-organism/lib/adjustArgs/extractFromDOM'
391 | ```
392 |
393 | Extract values from DOM, specifically:
394 | - For events as the first argument, extracts `value`, `checked`, and `name` from `event.target`. Additionally, if target has `data-` attributes, these will also be extracted in camelCase from its [`dataset`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset). Suffixing `data-` attributes with `_number` will convert value to a number (instead of string) using `parseFloat`, and drop the suffix. Handler will receive these extracted values in an object as the first argument, followed by the original arguments.
395 | - For `submit` events, extracts values of ` ` fields in a `