Repository: ctrlplusb/easy-peasy
Pull request #11 (2018-11-06 10:56:53)

ci: test Node.js 6, 8, 10 and 11
Node.js 6, 8 and 10 are the current LTS branches and 11 is stable. See https://github.com/nodejs/Release/blob/master/README.md

We should test them all to catch any issues.


Patch:
diff --git a/.travis.yml b/.travis.yml
index 56dc130..606f1cd 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -5,12 +5,14 @@ cache:
   directories:
     - node_modules
 node_js:
-- '8'
+  - 'node'
+  - '10'
+  - '8'
 script:
 # Unfortunately flow falls over when a dep exists in peer deps and others. :(
 # @see https://github.com/flowtype/flow-typed/issues/528
 #- yarn run flow:defs
-- npm run test
+- yarn run test
 after_success:
 # Deploy code coverage report to codecov.io
-- npm run test:coverage:deploy
+- yarn run test:coverage:deploy
diff --git a/README.md b/README.md
index 56b705f..a057e84 100644
--- a/README.md
+++ b/README.md
@@ -41,7 +41,7 @@ function TodoList() {
   const add = useAction(dispatch => dispatch.todos.add)
   return (
     <div>
-      {todos.map((todo, idx) => <div key={idx}>{todo.text}</div>)}
+      {todos.map((todo, idx) => <div key={idx}>{todo}</div>)}
       <AddTodo onAdd={add} />
     </div>
   )
@@ -86,6 +86,8 @@ function TodoList() {
     - [Consuming state in your Components](#consuming-state-in-your-components)
     - [Firing actions in your Components](#firing-actions-in-your-components)
     - [Alternative usage via react-redux](#alternative-usage-via-react-redux)
+  - [Usage with React Native](#usage-with-react-native)
+    - [Remote Redux Dev Tools](#remote-redux-dev-tools)
   - [API](#api)
     - [createStore(model, config)](#createstoremodel-config)
     - [action](#action)
@@ -417,6 +419,35 @@ export default connect(
 )(EditTodo)
 ```
 
+<p>&nbsp;</p>
+
+---
+
+## Usage with React Native
+
+Easy Peasy is platform agnostic but makes use of features that may not be available in all environments.
+
+### Remote Redux Dev Tools
+
+React Native, hybrid, desktop and server side Redux apps can use Redux Dev Tools using the [Remote Redux DevTools](https://github.com/zalmoxisus/remote-redux-devtools) library.
+
+To use this library, you will need to pass the DevTools compose helper as part of the [config object](#createstoremodel-config) to `createStore`
+
+```javascript
+import { createStore } from 'easy-peasy';
+import { composeWithDevTools } from 'remote-redux-devtools';
+import model from './model';
+
+const store = createStore(model, {
+  config: {
+    compose: composeWithDevTools({ realtime: true }),
+  }
+);
+```
+
+See [https://github.com/zalmoxisus/remote-redux-devtools#parameters](https://github.com/zalmoxisus/remote-redux-devtools#parameters) for all configuration options.
+
+
 <p>&nbsp;</p>
 
 ---
@@ -438,8 +469,12 @@ Creates a Redux store based on the given model. The model must be an object and
   - `config` (Object, not required)
 
     Provides custom configuration options for your store. It supports the following options:
+    
+    - `compose` (Function, not required, default=undefined)
 
-    - `devTool` (bool, not required, default=false)
+       Custom [`compose`](https://redux.js.org/api/compose) function that will be used in place of the one from Redux or Redux Dev Tools. This is especially useful in the context of React Native and other environments. See the Usage with React Native notes.
+
+    - `devTools` (bool, not required, default=true)
 
        Setting this to `true` will enable the [Redux Dev Tools Extension](https://github.com/zalmoxisus/redux-devtools-extension).
 
@@ -873,7 +908,7 @@ const EditTodo = ({ todo }) => {
 
 <p>&nbsp;</p>
 
----
+----
 
 ## Prior art
 
@@ -889,4 +924,4 @@ This library was massively inspired by the following awesome projects. I tried t
 
  - [mobx-state-tree](https://github.com/mobxjs/mobx-state-tree)
 
-   Model Driven State Management
\ No newline at end of file
+   Model Driven State Management
diff --git a/package.json b/package.json
index c3090b4..63a9e4d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "easy-peasy",
-  "version": "1.3.0",
+  "version": "1.4.0",
   "description": "Easy peasy global state for React",
   "license": "MIT",
   "main": "dist/easy-peasy.cjs.js",
@@ -57,6 +57,7 @@
     "babel-core": "^7.0.0-bridge.0",
     "babel-eslint": "^10.0.1",
     "babel-jest": "^23.6.0",
+    "babel-plugin-transform-react-remove-prop-types": "^0.4.20",
     "change-case": "^3.0.2",
     "codecov": "^3.1.0",
     "eslint": "^5.8.0",
@@ -78,7 +79,9 @@
     "rimraf": "^2.6.2",
     "rollup": "^0.67.0",
     "rollup-plugin-babel": "^4.0.3",
+    "rollup-plugin-commonjs": "^9.2.0",
     "rollup-plugin-filesize": "^5.0.1",
+    "rollup-plugin-node-resolve": "^3.4.0",
     "rollup-plugin-replace": "^2.1.0",
     "rollup-plugin-uglify": "^6.0.0"
   },
diff --git a/rollup.config.js b/rollup.config.js
index b9cca8e..e4cab08 100644
--- a/rollup.config.js
+++ b/rollup.config.js
@@ -6,6 +6,9 @@ const changeCase = require('change-case')
 const produce = require('immer').default
 const replace = require('rollup-plugin-replace')
 const fileSize = require('rollup-plugin-filesize')
+const resolve = require('rollup-plugin-node-resolve')
+const commonjs = require('rollup-plugin-commonjs')
+
 const packageJson = require('./package.json')
 
 process.env.BABEL_ENV = 'production'
@@ -14,7 +17,6 @@ const baseConfig = {
   external: [
     'immer',
     'memoize-one',
-    'prop-types',
     'react',
     'redux',
     'redux-thunk',
@@ -37,6 +39,7 @@ const baseConfig = {
         '@babel/plugin-syntax-import-meta',
         ['@babel/plugin-proposal-class-properties', { loose: false }],
         '@babel/plugin-proposal-json-strings',
+        ['transform-react-remove-prop-types', { removeImport: true }],
       ],
     }),
   ],
@@ -44,20 +47,19 @@ const baseConfig = {
 
 const commonUMD = config =>
   produce(config, draft => {
+    draft.external.splice(draft.external.indexOf('shallowequal'), 1)
     draft.output.format = 'umd'
     draft.output.globals = {
       'memoize-one': 'memoizeOne',
-      'prop-types': 'PropTypes',
-      'redux-thunk': 'thunk',
-      immer: 'produce',
+      'redux-thunk': 'ReduxThunk',
+      immer: 'immer',
       react: 'React',
-      redux: 'redux',
-      shallowequal: 'shallowEqual',
+      redux: 'Redux',
     }
     draft.output.name = changeCase
       .titleCase(packageJson.name.replace(/-/g, ' '))
       .replace(/ /g, '')
-    draft.plugins.push(fileSize())
+    draft.plugins.push(fileSize(), resolve(), commonjs())
   })
 
 module.exports = [
diff --git a/src/easy-peasy.js b/src/easy-peasy.js
index bf21ab7..2fea382 100644
--- a/src/easy-peasy.js
+++ b/src/easy-peasy.js
@@ -1,20 +1,20 @@
 import {
   applyMiddleware,
-  compose,
+  compose as reduxCompose,
   createStore as reduxCreateStore,
 } from 'redux'
 import memoizeOne from 'memoize-one'
 import produce from 'immer'
 import thunk from 'redux-thunk'
-import { isObject } from './lib'
+import { isStateObject } from './lib'
 
-const effectSymbol = Symbol('effect')
-const selectSymbol = Symbol('select')
-const selectDependeciesSymbol = Symbol('selectDependencies')
-const selectStateSymbol = Symbol('selectState')
+const effectSymbol = '__effect__'
+const selectSymbol = '__select__'
+const selectDependeciesSymbol = '__selectDependencies__'
+const selectStateSymbol = '__selectState__'
 
 const get = (path, target) =>
-  path.reduce((acc, cur) => (isObject(acc) ? acc[cur] : undefined), target)
+  path.reduce((acc, cur) => (isStateObject(acc) ? acc[cur] : undefined), target)
 
 const set = (path, target, value) => {
   path.reduce((acc, cur, idx) => {
@@ -52,6 +52,7 @@ export const createStore = (model, options = {}) => {
     middleware = [],
     initialState = {},
     injections,
+    compose,
   } = options
 
   const definition = {
@@ -124,7 +125,7 @@ export const createStore = (model, options = {}) => {
             }),
           )
         }
-      } else if (isObject(value) && Object.keys(value).length > 0) {
+      } else if (isStateObject(value) && Object.keys(value).length > 0) {
         extract(value, path)
       } else {
         // State
@@ -148,7 +149,7 @@ export const createStore = (model, options = {}) => {
       return acc
     }, [])
     const stateAtPath = Object.keys(current).reduce(
-      (acc, key) => (isObject(current[key]) ? [...acc, key] : acc),
+      (acc, key) => (isStateObject(current[key]) ? [...acc, key] : acc),
       [],
     )
     const nestedReducers = stateAtPath.map(key => [
@@ -228,11 +229,12 @@ export const createStore = (model, options = {}) => {
   const reducers = createReducers(actionReducers, [])
 
   const composeEnhancers =
-    devTools &&
+    compose ||
+    (devTools &&
     typeof window !== 'undefined' &&
     window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
       ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
-      : compose
+      : reduxCompose)
 
   const store = reduxCreateStore(
     reducers,
diff --git a/src/hooks.js b/src/hooks.js
index 20094c1..64942ca 100644
--- a/src/hooks.js
+++ b/src/hooks.js
@@ -1,19 +1,19 @@
-import { useState, useLayoutEffect, useContext } from 'react'
+import { useState, useEffect, useContext } from 'react'
 import shallowEqual from 'shallowequal'
 import EasyPeasyContext from './context'
-import { isObject } from './lib'
+import { isStateObject } from './lib'
 
 export function useStore(mapState) {
   const store = useContext(EasyPeasyContext)
   const [state, setState] = useState(mapState(store.getState()))
-  useLayoutEffect(() => {
+  useEffect(() => {
     let stateCache = state
     return store.subscribe(() => {
       const newState = mapState(store.getState())
       if (
         newState === stateCache ||
-        (isObject(newState) &&
-          isObject(stateCache) &&
+        (isStateObject(newState) &&
+          isStateObject(stateCache) &&
           shallowEqual(newState, stateCache))
       ) {
         // Do nothing
diff --git a/src/lib.js b/src/lib.js
index 656c0f5..54b635a 100644
--- a/src/lib.js
+++ b/src/lib.js
@@ -1,1 +1,2 @@
-export const isObject = x => x && typeof x === 'object' && !Array.isArray(x)
+export const isStateObject = x =>
+  x !== null && typeof x === 'object' && !Array.isArray(x)


Test patch:
diff --git a/src/__tests__/easy-peasy.test.js b/src/__tests__/easy-peasy.test.js
index adc5054..3e5430d 100644
--- a/src/__tests__/easy-peasy.test.js
+++ b/src/__tests__/easy-peasy.test.js
@@ -54,7 +54,10 @@ describe('react', () => {
     )
 
     // act
-    render(app)
+    const { rerender } = render(app)
+
+    // this triggers the effect to fire
+    rerender(app)
 
     // assert
     expect(renderSpy).toBeCalledTimes(1)
@@ -135,7 +138,10 @@ describe('react', () => {
       )
 
       // act
-      const { getByTestId } = render(app)
+      const { getByTestId, rerender } = render(app)
+
+      // this triggers the effect to fire
+      rerender(app)
 
       // assert
       const countButton = getByTestId('count')
@@ -218,7 +224,10 @@ describe('react', () => {
       )
 
       // act
-      const { getByTestId } = render(app)
+      const { getByTestId, rerender } = render(app)
+
+      // this triggers the effect to fire
+      rerender(app)
 
       // assert
       const countButton = getByTestId('count')
diff --git a/src/__tests__/lib.test.js b/src/__tests__/lib.test.js
new file mode 100644
index 0000000..af0f8b3
--- /dev/null
+++ b/src/__tests__/lib.test.js
@@ -0,0 +1,52 @@
+import { isStateObject } from '../lib'
+
+describe('isObject', () => {
+  test('anon functions', () => {
+    expect(isStateObject(() => undefined)).toBe(false)
+  })
+
+  test('functions', () => {
+    function foo() {}
+    expect(isStateObject(foo)).toBe(false)
+  })
+
+  // We won't fix this as you shouldn't be adding regexes to your state
+  test('regex', () => {
+    expect(isStateObject(/foo/)).toBe(true)
+  })
+
+  test('null', () => {
+    expect(isStateObject(null)).toBe(false)
+  })
+
+  test('string', () => {
+    expect(isStateObject('foo')).toBe(false)
+  })
+
+  test('number', () => {
+    expect(isStateObject(1)).toBe(false)
+  })
+
+  test('boolean', () => {
+    expect(isStateObject(true)).toBe(false)
+  })
+
+  test('object', () => {
+    expect(isStateObject({})).toBe(true)
+  })
+
+  test('class', () => {
+    class Foo {}
+    expect(isStateObject(Foo)).toBe(false)
+  })
+
+  test('class instance', () => {
+    class Foo {}
+    const foo = new Foo()
+    expect(isStateObject(foo)).toBe(true)
+  })
+
+  test('array', () => {
+    expect(isStateObject([])).toBe(false)
+  })
+})