diff --git a/.all-contributorsrc b/.all-contributorsrc
new file mode 100644
index 0000000..28e790e
--- /dev/null
+++ b/.all-contributorsrc
@@ -0,0 +1,59 @@
+{
+ "files": [
+ "README.md"
+ ],
+ "imageSize": 100,
+ "commit": false,
+ "contributors": [
+ {
+ "login": "solkimicreb",
+ "name": "Miklos Bertalan",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/6956014?v=4",
+ "profile": "https://bertalan-miklos.now.sh/",
+ "contributions": [
+ "code",
+ "test",
+ "doc",
+ "blog"
+ ]
+ },
+ {
+ "login": "rolandszoke",
+ "name": "Roland",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/14181908?v=4",
+ "profile": "https://github.com/rolandszoke",
+ "contributions": [
+ "code",
+ "test",
+ "doc"
+ ]
+ },
+ {
+ "login": "danielgrgly",
+ "name": "Daniel Gergely",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/22714514?s=460&u=cbbc2326d5f671693a1f33671d9eadad902d5191&v=4",
+ "profile": "https://github.com/danielgrgly",
+ "contributions": [
+ "code",
+ "design",
+ "example"
+ ]
+ },
+ {
+ "login": "peteyycz",
+ "name": "Peter Czibik",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/7130689?v=4",
+ "profile": "https://github.com/peteyycz",
+ "contributions": [
+ "infra"
+ ]
+ }
+ ],
+ "contributorsPerLine": 7,
+ "projectName": "react-easy-state",
+ "projectOwner": "RisingStack",
+ "repoType": "github",
+ "repoHost": "https://github.com",
+ "skipCi": true,
+ "badgeTemplate": "[](#contributors)"
+}
diff --git a/.babelrc b/.babelrc
deleted file mode 100644
index 38733e6..0000000
--- a/.babelrc
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "presets": ["react", "es2015", "stage-0"]
-}
diff --git a/.circleci/config.yml b/.circleci/config.yml
index bf5a14c..bf06d76 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -1,20 +1,56 @@
-# Javascript Node CircleCI 2.0 configuration file
-#
-# Check https://circleci.com/docs/2.0/language-javascript/ for more details
-#
version: 2
jobs:
- build:
+ test-and-build:
+ working_directory: ~/repo
docker:
- # specify the version you desire here
- - image: circleci/node:7.10
-
- # Specify service dependencies here if necessary
- # CircleCI maintains a library of pre-built images
- # documented at https://circleci.com/docs/2.0/circleci-images/
-
+ - image: circleci/node:12
steps:
- checkout
- run: npm install
+ - run: npm run lint
+ # run the tests on the original source to get coverage info
+ - run: npx commitlint --from HEAD~1 --to HEAD --verbose
- run: npm test
- run: npm run coveralls
+ # test the final bundled builds
+ - run: npm run build
+ - run: npm run test-builds
+ - persist_to_workspace:
+ root: ~/repo
+ paths: .
+
+ test-node-support:
+ working_directory: ~/repo
+ docker:
+ - image: circleci/node:6
+ steps:
+ - attach_workspace:
+ at: ~/repo
+ # test the final build in Node 6 for platform support
+ - run: npm run test-node-support
+
+ release:
+ working_directory: ~/repo
+ docker:
+ - image: circleci/node:12
+ steps:
+ - attach_workspace:
+ at: ~/repo
+ # test the final bundled builds again right before release
+ - run: npm run test-builds
+ - run: npm run semantic-release
+
+workflows:
+ version: 2
+ test_and_release:
+ jobs:
+ - test-and-build
+ - test-node-support:
+ requires:
+ - test-and-build
+ - release:
+ requires:
+ - test-node-support
+ filters:
+ branches:
+ only: master
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000..4016ec3
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,34 @@
+{
+ "parser": "babel-eslint",
+ "extends": ["airbnb", "prettier"],
+ "plugins": ["prettier"],
+ "rules": {
+ "prettier/prettier": ["error"],
+ "import/prefer-default-export": "off",
+ "import/no-extraneous-dependencies": "off",
+ "import/no-mutable-exports": "off",
+ "no-nested-ternary": "off",
+ "no-param-reassign": "off",
+ "func-names": "off",
+ "max-classes-per-file": "off",
+ "react/prefer-stateless-function": "off",
+ "react/destructuring-assignment": "off",
+ "react/state-in-constructor": "off",
+ "react/jsx-props-no-spreading": "off",
+ "react/prop-types": "off",
+ "react/jsx-one-expression-per-line": "off"
+ },
+ "globals": {
+ "window": true,
+ "EventTarget": true,
+ "WebSocket": true,
+ "describe": true,
+ "afterAll": true,
+ "test": true,
+ "expect": true,
+ "afterEach": true,
+ "document": true,
+ "jest": true,
+ "Event": true
+ }
+}
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..b77873c
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,31 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: bug
+assignees: ''
+
+---
+
+**React Easy State version:** x.x.x
+**Platform:** browser / node / react-native / other
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+---
+
+### For tougher bugs
+
+**To Reproduce**
+Steps to reproduce the behavior.
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+---
+
+### For the toughest bugs only
+
+**[CodeSandbox](https://codesandbox.io/s/github/RisingStack/react-easy-state/tree/master/examples/stop-watch) reproduction**
+A demo application that showcases the bug.
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..4427295
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,8 @@
+blank_issues_enabled: true
+contact_links:
+ - name: Question
+ url: https://stackoverflow.com/questions/ask?tags=react-easy-state+reactjs+react-state-management
+ about: Stack Overflow questions with the (prefilled) react-easy-state tag will be answered by the team
+ - name: Chat
+ url: https://gitter.im/RisingStack/react-easy-state
+ about: Chat about ideas and patterns with the community
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..2a6ce87
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: enhancement
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is.
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered or seen elsewhere**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context about the feature request here.
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000..5a3d712
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,7 @@
+Resolves #
+
+Proposed changes:
+-
+-
+
+Please follow the [angular commit message format](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#-commit-message-guidelines). Example: `docs(readme): fix title typo`
diff --git a/.gitignore b/.gitignore
index 04917b9..c205594 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,12 @@
+.DS_Store
+
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
+nohup.out
# Runtime data
pids
@@ -36,6 +39,9 @@ build/Release
node_modules/
jspm_packages/
+# Builds
+/dist
+
# Typescript v1 declaration files
typings/
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..b43bf86
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1 @@
+README.md
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..1b88769
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,6 @@
+{
+ "semi": true,
+ "trailingComma": "all",
+ "singleQuote": true,
+ "printWidth": 70
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..2f584e7
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,6 @@
+{
+ "files.insertFinalNewline": true,
+ "files.trimFinalNewlines": true,
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
+ "editor.formatOnSave": true
+}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..bf28ad5
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,29 @@
+# Contributing
+
+## Issues
+
+Issues are precious to this project.
+
+- **Ideas** are a valuable source of contributions others can make
+- **Problems** show where this project is lacking
+- With a **question**, you show where contributors can improve the user experience
+
+Thank you for creating them.
+
+## Commits
+
+Your commit messages should follow Angular [commit convention](https://www.conventionalcommits.org/en/v1.0.0-beta.4/). Therefore we are linting your commits with [Husky](https://github.com/typicode/husky), meaning you cannot create a non-conventional commit message. If you squash merge your pull request, please make sure you also follow the commit convention in your squashed commit message.
+
+## Pull Requests
+
+Pull requests are a great way to get your ideas into this repository.
+You should be clear which problem you're trying to solve with your contribution.
+Every pull request will require an approved review before merging.
+
+## Linters
+
+This project is using [ESLint](https://eslint.org/) for linting and [Prettier](https://prettier.io/) for code formatting. Please follow their standards on contributing. Your code is automatically formatted on saving a file when using [VSCode](https://code.visualstudio.com/) or [Atom](https://atom.io/). [Husky](https://github.com/typicode/husky) ensures that your changes are properly linted before making a commit. If you want to lint manually, you can use `lint` and `lint --fix` npm scripts to lint the source code and the test files.
+
+## Tests
+
+To run our tests use `npm t` script. We are using [Jest](https://jestjs.io/) and [React Testing Library](https://github.com/testing-library/react-testing-library) for testing. Consider creating new test cases when necessary under the `__tests__` folder, we want to keep our test covarage above 90%. Use `.test.js` or `.test.jsx` suffix for test files and `.test.native.js` or `.test.native.jsx` for native tests.
diff --git a/LICENSE b/LICENSE
index 2702822..93bcfeb 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2017 Miklos Bertalan
+Copyright (c) 2019 Miklos Bertalan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 21f1714..c1ece1a 100644
--- a/README.md
+++ b/README.md
@@ -1,205 +1,880 @@
-# React Easy State
+
+
+
+
+## React Easy State - by [RisingStack](https://risingstack.com/)
+
+[](https://circleci.com/gh/RisingStack/react-easy-state/tree/master) [](https://david-dm.org/RisingStack/react-easy-state) [](https://coveralls.io/github/RisingStack/react-easy-state?branch=master) [](https://bundlephobia.com/result?p=@risingstack/react-easy-state) [](https://www.npmjs.com/package/@risingstack/react-easy-state) [](https://www.npmjs.com/package/@risingstack/react-easy-state)
+[](#contributors)
+
+
+
+
+**NEWS:** v6.3.0 fixed a nasty bug that could render [zombie children](https://react-redux.js.org/api/hooks#stale-props-and-zombie-children). Please update to this version at least to save yourself some headaches. Thanks!
+
+
+Table of Contents
+
+
+
+
+* [Introduction :wave:](#introduction-wave)
+* [Installation :cd:](#installation-cd)
+* [Everyday Usage :sunglasses:](#everyday-usage-sunglasses)
+ + [Creating global stores](#creating-global-stores)
+ + [Creating reactive views](#creating-reactive-views)
+ + [Creating local stores](#creating-local-stores)
+* [Advanced Usage :nerd_face:](#advanced-usage-nerd_face)
+ + [Adding side effects](#adding-side-effects)
+* [API Summary :book:](#api-summary-book)
+ + [store(obj)](#storeobj)
+ + [view(Comp)](#viewcomp)
+ + [batch(fn)](#batchfn)
+ + [autoEffect(fn)](#autoeffectfn)
+ + [clearEffect(fn)](#cleareffectfn)
+* [Examples with live demos :tv:](#examples-with-live-demos-tv)
+* [Articles :loudspeaker:](#articles-loudspeaker)
+* [Performance :rocket:](#performance-rocket)
+* [Platform support :computer:](#platform-support-computer)
+* [Alternative builds :wrench:](#alternative-builds-wrench)
+* [Contributors :sparkles:](#contributors-sparkles)
+
+
+
+
+
+## Introduction :wave:
+
+React Easy State is a practical state management library with two functions and two accompanying rules.
+
+1. Always wrap your components with `view()`.
+2. Always wrap your state store objects with `store()`.
+
+```jsx
+import React from 'react';
+import { store, view } from '@risingstack/react-easy-state';
+
+const counter = store({
+ num: 0,
+ increment: () => counter.num++
+});
+
+export default view(() => (
+
+));
+```
-[](https://circleci.com/gh/solkimicreb/react-easy-state/tree/master) [](https://coveralls.io/github/solkimicreb/react-easy-state) [](https://standardjs.com) [](https://www.npmjs.com/package/react-easy-state) [](https://david-dm.org/solkimicreb/react-easy-state) [](https://www.npmjs.com/package/react-easy-state)
+This is enough for it to automatically update your views when needed. It doesn't matter how you structure or mutate your state stores, any syntactically valid code works.
-*Easy State provides a healthy balance of local and global state management in a simple, scalable way.*
+Check this [TodoMVC codesandbox](https://codesandbox.io/s/github/RisingStack/react-easy-state/tree/master/examples/todo-mvc?module=%2Fsrc%2FtodosStore.js) or [raw code](/examples/todo-mvc/src/todosStore.js) for a more exciting example with nested data, arrays and getter properties.
-## Table of contents
+## Installation :cd:
-- [Installation](#installation)
-- [Usage](#usage)
-- [Examples](#examples-with-live-demos)
-- [Platform support](#platform-support)
-- [Performance](#performance)
-- [State management overview](#state-management-overview)
-- [How does it work?](#how-does-it-work)
-- [Contributing](#contributing)
-- [The NX Framework](#the-nx-framework)
+`npm install @risingstack/react-easy-state`
-## Installation
+
+Setting up a quick project
+
-`npm install react-easy-state`
+Easy State supports Create React App without additional configuration. Just run the following commands to get started.
-## Usage
+```sh
+npx create-react-app my-app
+cd my-app
+npm install @risingstack/react-easy-state
+npm start
+```
-Easy State consists of two functions:
+_You need npm 5.2+ to use npx._
-- `easyComp` makes React's own component level state management simpler.
-- `easyStore` creates global state stores for complex apps.
+
-### easyComp
+## Everyday Usage :sunglasses:
-Wrapping your components with the `easyComp` function provides the following benefits.
+### Creating global stores
-- It allows you to mutate the component's state directly, without calling `setState`.
+`store` creates a state store from the passed object and returns it. A state store behaves just like the passed object. (To be precise, it is a transparent reactive proxy of the original object.)
-- It binds your component's methods to the component.
+```js
+import { store } from '@risingstack/react-easy-state';
+
+const user = store({ name: 'Rick' });
+// stores behave like normal JS objects
+user.name = 'Bob';
+```
+
+
+State stores may have arbitrary structure and they may be mutated in any syntactically valid way.
+
```js
-import React, { Component } from 'react'
-import { easyComp } from 'react-easy-state'
+import { store } from '@risingstack/react-easy-state';
+
+// stores can include any valid JS structure
+// including nested data, arrays, Maps, Sets, getters, setters, inheritance, ...
+const user = store({
+ profile: {
+ firstName: 'Bob',
+ lastName: 'Smith',
+ get name() {
+ return `${user.profile.firstName} ${user.profile.lastName}`;
+ },
+ },
+ hobbies: ['programming', 'sports'],
+ friends: new Map(),
+});
+
+// stores may be mutated in any syntactically valid way
+user.profile.firstName = 'Bob';
+delete user.profile.lastName;
+user.hobbies.push('reading');
+user.friends.set('id', otherUser);
+```
-class Hello extends Component {
- state = {
- name: 'World!'
- }
+
+
+
+
+Async operations can be expressed with the standard async/await syntax.
+
+
+```js
+import { store } from '@risingstack/react-easy-state';
+
+const userStore = store({
+ user: {},
+ async fetchUser() {
+ userStore.user = await fetch('/user');
+ },
+});
+
+export default userStore;
+```
+
+
+
+
+
+State stores may import and use other state stores in their methods.
+
+
+_userStore.js_
+
+```js
+import { store } from '@risingstack/react-easy-state';
+
+const userStore = store({
+ user: {},
+ async fetchUser() {
+ userStore.user = await fetch('/user');
+ },
+});
+
+export default userStore;
+```
+
+_recipesStore.js_
+
+```js
+import { store } from '@risingstack/react-easy-state';
+import userStore from './userStore';
+
+const recipesStore = store({
+ recipes: [],
+ async fetchRecipes() {
+ recipesStore.recipes = await fetch(
+ `/recipes?user=${userStore.user.id}`,
+ );
+ },
+});
+
+export default recipesStore;
+```
+
+
+
+
+
+Wrap your state stores with store as early as possible.
+
+
+```js
+// DON'T DO THIS
+const person = { name: 'Bob' };
+person.name = 'Ann';
+
+export default store(person);
+```
+
+```js
+// DO THIS INSTEAD
+const person = store({ name: 'Bob' });
+person.name = 'Ann';
+
+export default person;
+```
+
+The first example wouldn't trigger re-renders on the `person.name = 'Ann'` mutation, because it is targeted at the raw object. Mutating the raw - none `store`-wrapped object - won't schedule renders.
+
+
+
+
+
+Avoid using the this keyword in the methods of your state stores.
+
+
+```jsx
+import { store, view } from '@risingstack/react-easy-state';
+
+const counter = store({
+ num: 0,
+ increment() {
+ // DON'T DO THIS
+ this.num++;
+ // DO THIS INSTEAD
+ counter.num++;
+ },
+});
+
+export default view(() => (
+
{counter.num}
+));
+```
+
+`this.num++` won't work, because `increment` is passed as a callback and loses its `this`. You should use the direct object reference - `counter` - instead of `this`.
+
+
+
+### Creating reactive views
+
+Wrapping your components with `view` turns them into reactive views. A reactive view re-renders whenever a piece of store - used inside its render - changes.
+
+```jsx
+import React from 'react';
+import { view, store } from '@risingstack/react-easy-state';
+
+// this is a global state store
+const user = store({ name: 'Bob' });
+
+// this is re-rendered whenever user.name changes
+export default view(() => (
+
+ (user.name = ev.target.value)}
+ />
+
Hello {user.name}!
+
+));
+```
+
+
+Wrap ALL of your components with view - including class and function ones - even if they don't seem to directly use a store.
+
+
+Every component that is using a store or part of a store inside its render must be wrapped with `view`. Sometimes store usage is not so explicit and easy to to miss.
+
+```jsx
+import { view, store } from '@risingstack/react-easy-state';
+
+const appStore = store({
+ user: { name: 'Ann' },
+});
+
+const App = view(() => (
+
+
My App
+
+
+));
+
+// DO THIS
+const Profile = view(({ user }) =>
Name: {user.name}
);
+
+// DON'T DO THIS
+// This won't re-render on appStore.user.name = 'newName' like mutations
+const Profile = ({ user }) =>
Name: {user.name}
;
+```
+
+If you are **100% sure** that your component is not using any stores you can skip the `view` wrapper.
+
+```jsx
+import React from 'react';
+
+// you don't have to wrap this component with `view`
+export default (() =>
This is just plain text
);
+```
+
+`view` wrapping is advised even in these cases though.
+
+- It saves you from future headaches as your project grows and you start to use stores inside these components.
+- `view` is pretty much equivalent to `memo` if you don't use any stores. That is nearly always nice to have.
+
+
+
+
+
+A single reactive component may use multiple stores inside its render.
+
+
+```jsx
+import React from 'react';
+import { view, store } from '@risingstack/react-easy-state';
+
+const user = store({ name: 'Bob' });
+const timeline = store({ posts: ['react-easy-state'] });
+
+// this is re-rendered whenever user.name or timeline.posts[0] changes
+export default view(() => (
+
+
Hello {user.name}!
+
Your first post is: {timeline.posts[0]}
+
+));
+```
+
+
+
+
+
+view implements an optimal shouldComponentUpdate (or memo) for your components.
+
+
+- Using `PureComponent` or `memo` will provide no additional performance benefits.
+
+- Defining a custom `shouldComponentUpdate` may rarely provide performance benefits when you apply some use case specific heuristics inside it.
+
+
+
+
+
+Reactive renders are batched. Multiple synchronous store mutations won't result in multiple re-renders of the same component.
+
+
+```jsx
+import React from 'react';
+import { view, store } from '@risingstack/react-easy-state';
+
+const user = store({ name: 'Bob', age: 30 });
+
+function mutateUser() {
+ user.name = 'Ann';
+ user.age = 32;
+}
+
+// calling `mutateUser` will only trigger a single re-render of the below component
+// even though it mutates the store two times in quick succession
+export default view(() => (
+
+ name: {user.name}, age: {user.age}
+
+));
+```
+
+If you mutate your stores multiple times synchronously from **exotic task sources**, multiple renders may rarely happen. If you experience performance issues you can batch changes manually with the `batch` function. `batch(fn)` executes the passed function immediately and batches any subsequent re-renders until the function execution finishes.
+
+```jsx
+import React from 'react';
+import { view, store, batch } from '@risingstack/react-easy-state';
+
+const user = store({ name: 'Bob', age: 30 });
+
+function mutateUser() {
+ // this makes sure the state changes will cause maximum one re-render,
+ // no matter where this function is getting invoked from
+ batch(() => {
+ user.name = 'Ann';
+ user.age = 32;
+ });
+}
+
+export default view(() => (
+
+ name: {user.name}, age: {user.age}
+
+));
+```
+
+> **NOTE:** The React team plans to improve render batching in the future. The `batch` function and built-in batching may be deprecated and removed in the future in favor of React's own batching.
+
+
+
+
+
+Always apply view as the latest (innermost) wrapper when you combine it with other Higher Order Components.
+
+
+```jsx
+import { view } from '@risingstack/react-easy-state';
+import { withRouter } from 'react-router-dom';
+import { withTheme } from 'styled-components';
+
+const Comp = () =>
A reactive component
;
+
+// DO THIS
+withRouter(view(Comp));
+withTheme(view(Comp));
+
+// DON'T DO THIS
+view(withRouter(Comp));
+view(withTheme(Comp));
+```
+
+
+
+
+
+Usage with (pre v4.4) React Router.
+
+
+- If routing is not updated properly, wrap your `view(Comp)` - with the `Route`s inside - in `withRouter(view(Comp))`. This lets react-router know when to update.
+
+- The order of the HOCs matter, always use `withRouter(view(Comp))`.
+
+This is not necessary if you use React Router 4.4+. You can find more details and some reasoning about this in [this react-router docs page](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/guides/blocked-updates.md).
+
+
+
+
+
+Usage with React Developer Tools.
+
+
+If you want React Developer Tools to recognize your reactive view components' names, you have to pass either a **named function** or an anonymous function with **name inference** to the `view` wrapper.
+
+```jsx
+import React from 'react';
+import { view, store } from '@risingstack/react-easy-state';
+
+const user = store({
+ name: 'Rick',
+});
+
+const componentName = () => (
+
{user.name}
+);
+
+export default view(componentName);
+```
+
+
+
+
+
+Passing nested data to third party components.
+
+
+Third party helpers - like data grids - may consist of many internal components which can not be wrapped by `view`, but sometimes you would like them to re-render when the passed data mutates. Traditional React components re-render when their props change by reference, so mutating the passed reactive data won't work in these cases. You can solve this issue by deep cloning the observable data before passing it to the component. This creates a new reference for the consuming component on every store mutation.
+
+```jsx
+import React from 'react';
+import { view, store } from '@risingstack/react-easy-state';
+import Table from 'rc-table';
+import cloneDeep from 'lodash/cloneDeep';
+
+const dataStore = store({
+ items: [
+ {
+ product: 'Car',
+ value: 12,
+ },
+ ],
+});
+
+export default view(() => (
+
+));
+```
+
+
+
+### Creating local stores
+
+A singleton global store is perfect for something like the current user, but sometimes having local component states is a better fit. Just create a store inside a function component or as a class component property in these cases.
+
+#### Local stores in function components
+
+```jsx
+import React from 'react'
+import { view, store } from '@risingstack/react-easy-state'
+
+export default view(() => {
+ const counter = store({ num: 0 })
+ const increment = () => counter.num++
+ return
+})
+```
+
+**Local stores in functions rely on React hooks. They require React and React DOM v16.8+ or React Native v0.59+ to work.**
+
+
+You can use React hooks - including useState - in function components, Easy State won't interfere with them. Consider using autoEffect instead of the useEffect hook for the best experience though.
+
- // this is bound to the component, so it can be safely passed as a callback
- onChange (ev) {
- // the state can be modified directly
- this.state.name = ev.target.value
+```jsx
+import React from 'react';
+import { view, store } from '@risingstack/react-easy-state';
+
+export default view(() => {
+ const [name, setName] = useState('Ann');
+ const user = store({ age: 30 });
+ return (
+
+ );
+});
+```
+
+
+
+#### Local stores in class components
+
+```jsx
+import React, { Component } from 'react';
+import { view, store } from '@risingstack/react-easy-state';
+
+class Counter extends Component {
+ counter = store({ num: 0 });
+ increment = () => counter.num++;
+
+ render() {
+ return (
+
+ );
}
+}
+
+export default view(Counter);
+```
- // the render is triggered whenever state.name changes
- render () {
- const { onChange } = this
- const { name } = this.state
+
+You can also use vanilla setState in your class components, Easy State won't interfere with it.
+
+```jsx
+import React, { Component } from 'react';
+import { view, store } from '@risingstack/react-easy-state';
+
+class Profile extends Component {
+ state = { name: 'Ann' };
+ user = store({ age: 30 });
+
+ setName = ev => this.setState({ name: ev.target.value });
+ setAge = ev => (this.user.age = ev.target.value);
+
+ render() {
return (
-
-
Hello {name}!
+
+
- )
+ );
}
}
-// the component must be wrapped with easyComp
-export default easyComp(Hello)
+export default view(Profile);
+```
+
+
+
+
+
+Don't name local stores as state. It may conflict with linter rules, which guard against direct state mutations.
+
+
+```jsx
+import React, { Component } from 'react';
+import { view, store } from '@risingstack/react-easy-state';
+
+class Profile extends Component {
+ // DON'T DO THIS
+ state = store({});
+ // DO THIS
+ user = store({});
+ render() {}
+}
```
-**Make sure to wrap all of your components with `easyComp` - including stateful and stateless ones - before you export them.**
+
+
-In addition to the boilerplate reduction, `easyComp` comes with a bunch of additional benefits that may not be obvious at first glance.
+
+Deriving local stores from props (getDerivedStateFromProps).
+
-- The state becomes a simple object, which updates synchronously. You don't have to worry about immutable state updates or the asynchronous nature of `setState`. If you are not sure about the meaning of this check out [this article](https://medium.freecodecamp.org/functional-setstate-is-the-future-of-react-374f30401b6b) about `setState`.
+Class components wrapped with `view` have an extra static `deriveStoresFromProps` lifecycle method, which works similarly to the vanilla `getDerivedStateFromProps`.
-- The render method is only triggered if it is affected by the state mutations. If it doesn't use the mutated part of the state or the mutation doesn't change the state, the render method is not triggered.
+```jsx
+import React, { Component } from 'react';
+import { view, store } from '@risingstack/react-easy-state';
-- The render method is never executed immediately. Triggered renders are collected and executed asynchronously in one batch.
+class NameCard extends Component {
+ userStore = store({ name: 'Bob' });
-- Renders always run before the next repaint.
+ static deriveStoresFromProps(props, userStore) {
+ userStore.name = props.name || userStore.name;
+ }
-- Duplicates renders are removed. A render never runs twice in one batch - no matter how many times it got triggered.
+ render() {
+ return
{this.userStore.name}
;
+ }
+}
-- Renders may trigger others renders by mutating the state. In this case loops are automatically resolved.
+export default view(NameCard);
+```
-- Easy State implements an optimal `shouldComponentUpdate` for your components.
+Instead of returning an object, you should directly mutate the received stores. If you have multiple local stores on a single component, they are all passed as arguments - in their definition order - after the first props argument.
-As a result the state is always fresh and a stable and a fresh view is always achieved before the next repaint with the minimal number of required renders.
+
-### easyStore
+## Advanced Usage :nerd_face:
-`easyStore` creates global state stores, to handle data that do not fit into component state. Wrapping an object with `easyStore` has to following effects.
+### Adding side effects
-- It transforms the object into a reactive data store, which triggers appropriate renders on mutations.
+Use `autoEffect` to react with automatic side effect to your store changes. Auto effects should contain end-of-chain logic - like changing the document title or saving data to LocalStorage. `view` is a special auto effect that does rendering.
-- It binds your object's methods to the object.
+
+Never use auto effects to derive data from other data. Use dynamic getters instead.
+
-```js
+```jsx
+import { store, autoEffect } from '@risingstack/react-easy-state';
+
+// DON'T DO THIS
+const store1 = store({ name: 'Store 1' })
+const store2 = store({ name: 'Store 2' })
+autoEffect(() => store2.name = store1.name)
+
+// DO THIS INSTEAD
+const store1 = store({ name: 'Store 1' })
+const store2 = store({ get name () { return store1.name } })
+```
+
+
+
+
+#### Global auto effects
+
+Global auto effects can be created with `autoEffect` and cleared up with `clearEffect`.
+
+```jsx
+import { store, autoEffect, clearEffect } from '@risingstack/react-easy-state';
+
+const app = store({ name: 'My App' })
+const effect = autoEffect(() => document.title = app.name)
+
+// this also updates the document title
+app.name = 'My Awesome App'
+
+clearEffect(effect)
+// this won't update the document title, the effect is cleared
+app.name = 'My App'
+```
+
+#### Local auto effects in function components
+
+Use local auto effects in function components instead of the `useEffect` hook when reactive stores are used inside them. These local effects are automatically cleared when the component unmounts.
+
+```jsx
import React from 'react'
-import { easyComp, easyStore } from 'react-easy-state'
-
-// this creates a global state store
-const store = easyStore({
- name: 'Hello',
- // this is bound to the object, so it can be safely passed as a callback
- setName (ev) {
- this.name = ev.target.value
- }
+import { store, view, autoEffect } from '@risingstack/react-easy-state';
+
+export default view(() => {
+ const app = store({ name: 'My App' })
+ // no need to clear the effect
+ autoEffect(() => document.title = app.name)
})
+```
-// the render is triggered whenever store.name changes
-function Hello () {
- const { name, setName } = store
+
+Explicitly pass none reactive dependencies - like vanillas props and state - to local auto effects in function components.
+
- return (
-
-
-
Hello {name}!
-
- )
+Because of the design of React hooks you have to explicitly pass all none reactive data to a hook-like dependency array. This makes sure that the effect also runs when the none reactive data changes.
+
+```jsx
+import React from 'react'
+import { store, view, autoEffect } from '@risingstack/react-easy-state';
+
+export default view(({ greeting }) => {
+ const app = store({ name: 'My App' })
+ // pass `greeting` in the dependency array because it is not coming from a store
+ autoEffect(() => document.title = `${greeting} ${app.name}`, [greeting])
+})
+```
+
+
+
+
+#### Local auto effects in class components
+
+Local effects in class components must be cleared when the component unmounts.
+
+```jsx
+import React, { Component } from 'react'
+import { store, view, autoEffect } from '@risingstack/react-easy-state';
+
+class App extends Component {
+ app = store({ name: 'My App' })
+
+ componentDidMount () {
+ this.effect = autoEffect(() => document.title = this.app.name)
+ }
+
+ componentWillUnmount () {
+ // local effects in class components must be cleared on unmount
+ clearEffect(this.effect)
+ }
}
+```
+
+## API Summary :book:
+
+### store(obj)
+
+Creates an observable store from the passed object and returns it. Can be used outside components for [global stores](#creating-global-stores) and inside components for [local stores]((#creating-local-stores)).
-// the component must be wrapped with easyComp
-export default easyComp(Hello)
+```js
+import { store } from '@risingstack/react-easy-state';
+
+const user = store({ name: 'Rick' });
```
-**Make sure to wrap your component with `easyComp` even if it uses global stores only and no local state.**
+### view(Comp)
-- Global stores are simple objects and there is no limitation on what you can do with them. As an example feel free to use expando properties, arrays, deeply nested objects, ES6 collections or getters/setters in your stores.
+Creates a [reactive view](#creating-reactive-views) from the passed component and returns it. A reactive view re-renders whenever any store data used inside it is mutated.
-- Render methods are only triggered if they are affected by the store mutations. If they don't use the mutated part of the store or the mutation doesn't change the store, the render method is not triggered.
+```jsx
+import React from 'react';
+import { view, store } from '@risingstack/react-easy-state';
-- Render methods are never executed immediately. Triggered renders are collected and executed asynchronously in one batch.
+const user = store({ name: 'Bob' });
-- Renders always run before the next repaint.
+export default view(() => (
+
Hello {user.name}!
+));
+```
-- Duplicates renders are removed. A render never runs twice in one batch - no matter how many times it got triggered.
+### batch(fn)
-- Renders may trigger others renders by mutating the store again. In this case loops are automatically resolved.
+Immediately executes the passed function and batches all store mutations inside it. Batched mutations are guaranteed to not trigger unnecessary double renders. Most task sources are batched automatically, only use `batch` if you encounter performance issues.
-As a result the stores are always fresh and a stable and a fresh view is always achieved before the next repaint with the minimal number of required renders.
+```jsx
+import React from 'react';
+import { view, store } from '@risingstack/react-easy-state';
-## Examples with live demos
+const user = store({ name: 'Bob' });
-- [Clock Widget](https://solkimicreb.github.io/react-easy-state/dist/clock.html) ([source](/examples/clock/))
-- [TodoMVC](https://solkimicreb.github.io/react-easy-state/dist/todoMVC.html) ([source](/examples/todoMVC/))
-- [Contacts Table](https://solkimicreb.github.io/react-easy-state/dist/contacts.html) ([source](/examples/contacts/))
+function setName() {
+ batch(() => {
+ user.name = 'Rick'
+ user.name = 'Ann'
+ })
+}
+```
-## Platform support
+### autoEffect(fn)
-- Node: 6 and above
-- Chrome: 49 and above
-- Firefox: 38 and above
-- Safari: 10 and above
-- Edge: 12 and above
-- Opera: 36 and above
-- React native is not yet supported
-- IE is not supported
+Creates a reactive function from the passed one, immediately executes it, and returns it. A reactive function automatically re-reruns whenever any store data used inside it is mutated.
-## Performance
+Can be used both [outside](#global-auto-effects) and [inside](#local-auto-effects-in-function-components) components.
-You can compare Easy State with plain React and other state management libraries with the below benchmarks. Easy State performs a bit better than MobX, a bit worse than plain optimized React and similarly to Redux.
+```js
+import { store, autoEffect } from '@risingstack/react-easy-state';
-- [js-framework-benchmark](https://github.com/krausest/js-framework-benchmark) ([source](https://github.com/krausest/js-framework-benchmark/tree/master/react-v15.5.4-easy-state-v1.0.3)) ([results](https://rawgit.com/krausest/js-framework-benchmark/master/webdriver-ts-results/table.html))
+const user = store({ name: 'Bob' })
-The list of benchmarks will expand in the future.
+autoEffect(() => document.title = user.name)
+```
-## State management overview
+### clearEffect(fn)
-Finding the right balance between local component state and global state stores is not always a trivial task. This section gives you some general guide lines when to use which.
+Takes a reactive function (returned by `autoEffect`) and clears the reactivity from it. Cleared reactive functions will no longer re-rerun on related store mutations. Reactive functions created inside function components are automatically cleared when the component unmounts.
-### Reusable widgets
+```js
+import { store, autoEffect, clearEffect } from '@risingstack/react-easy-state';
+
+const user = store({ name: 'Bob' })
+
+const effect = autoEffect(() => document.title = user.name)
+clearEffect(effect)
+```
-This is an easy decision. Always use local component state for reusable components. Depending on global stores would interfere with their reusability and break them. Check out the introductory [clock example](/examples/clock/) for some code.
+## Examples with live demos :tv:
-### Application state
+#### Beginner
-Application state should usually be managed in global stores. It is singleton and its is usually persistent while the app is open. You can find a few candidates for global storage below.
+- [Clock Widget](https://risingstack.github.io/react-easy-state/examples/clock/build) ([source](/examples/clock/)) ([codesandbox](https://codesandbox.io/s/github/RisingStack/react-easy-state/tree/master/examples/clock)) ([react-native source](/examples/native-clock/)) ([react-native sandbox](https://snack.expo.io/@git/github.com/RisingStack/react-easy-state:examples/native-clock)): a reusable clock widget with a tiny local state store.
+- [Stopwatch](https://risingstack.github.io/react-easy-state/examples/stop-watch/build) ([source](/examples/stop-watch/)) ([codesandbox](https://codesandbox.io/s/github/RisingStack/react-easy-state/tree/master/examples/stop-watch)) ([tutorial](https://hackernoon.com/introducing-react-easy-state-1210a156fa16)): a stopwatch with a mix of normal and getter state properties.
-- The currently logged in user is a good example. There is only one user at a time and user data should be easily available anywhere anytime. It is a perfect candidate for a singleton global store.
+#### Advanced
-- User inputs, which should go into the URL or change the browser history are also great examples. These are inherently global because they affect global concepts - like the URL and browser history. Some example for these are filters, date ranges and sorting primitives.
+- [Pokédex](https://risingstack.github.io/react-easy-state/examples/pokedex/build) ([source](/examples/pokedex/)) ([codesandbox](https://codesandbox.io/s/github/RisingStack/react-easy-state/tree/master/examples/pokedex)): a Pokédex app build with Apollo GraphQL, async actions and a global state.
+- [TodoMVC](https://risingstack.github.io/react-easy-state/examples/todo-mvc/build) ([source](/examples/todo-mvc/)) ([codesandbox](https://codesandbox.io/s/github/RisingStack/react-easy-state/tree/master/examples/todo-mvc)): a classic TodoMVC implementation with a lot of getters/setters and implicit reactivity.
+- [Contacts Table](https://risingstack.github.io/react-easy-state/examples/contacts/build) ([source](/examples/contacts/)) ([codesandbox](https://codesandbox.io/s/github/RisingStack/react-easy-state/tree/master/examples/contacts)): a data grid implementation with a mix of global and local state.
+- [Beer Finder](https://risingstack.github.io/react-easy-state/examples/beer-finder/build) ([source](/examples/beer-finder/)) ([codesandbox](https://codesandbox.io/s/github/RisingStack/react-easy-state/tree/master/examples/beer-finder)) ([tutorial](https://medium.com/@solkimicreb/design-patterns-with-react-easy-state-830b927acc7c)): an app with async actions and a mix of local and global state, which finds matching beers for your meal.
+
+## Articles :loudspeaker:
+
+- [Introducing React Easy State](https://blog.risingstack.com/introducing-react-easy-state/): making a simple stopwatch.
+- [Stress Testing React Easy State](https://medium.com/@solkimicreb/stress-testing-react-easy-state-ac321fa3becf): demonstrating Easy State's reactivity with increasingly exotic state mutations.
+- [Design Patterns with React Easy State](https://medium.com/@solkimicreb/design-patterns-with-react-easy-state-830b927acc7c): demonstrating async actions and local and global state management through a beer finder app.
+- [The Ideas Behind React Easy State](https://medium.com/dailyjs/the-ideas-behind-react-easy-state-901d70e4d03e): a deep dive under the hood of Easy State.
+
+## Performance :rocket:
+
+You can compare Easy State with plain React and other state management libraries with the below benchmarks. It performs a bit better than MobX and similarly to Redux.
+
+- [js-framework-benchmark](https://github.com/krausest/js-framework-benchmark) ([source](https://github.com/krausest/js-framework-benchmark/tree/master/react-v16.1.0-easy-state-v4.0.1-keyed)) ([results](https://rawgit.com/krausest/js-framework-benchmark/master/webdriver-ts-results/table.html))
+
+## Platform support :computer:
+
+- Node: 6 and above
+- Chrome: 49 and above
+- Firefox: 38 and above
+- Safari: 10 and above
+- Edge: 12 and above
+- Opera: 36 and above
+- React Native: 0.59 and above
-Not everything fits in global stores though. You can find a few cases below when using the local component state makes more sense then global stores.
+_This library is based on non polyfillable ES6 Proxies. Because of this, it will never support IE._
-- Utility and meta data should go into component state. For example you may have a component which handles recent history for an input field. It may make sense to receive the data for the input from a global store and manage the history meta data in the local state. Check out the [contacts table example](/examples/contacts/) for some code.
+## Alternative builds :wrench:
-### Application pages
+This library detects if you use ES6 or commonJS modules and serve the right format to you. The default bundles use ES6 features, which may not yet be supported by some minifier tools. If you experience issues during the build process, you can switch to one of the ES5 builds from below.
-Application pages deserve a special mention. A typical app has several pages but, only one of them is active at a time. This makes them a nice candidate for singleton global state stores. However pages are not as persistent as the app's user for example, which makes them lean towards local state management.
+- `@risingstack/react-easy-state/dist/es.es6.js` exposes an ES6 build with ES6 modules.
+- `@risingstack/react-easy-state/dist/es.es5.js` exposes an ES5 build with ES6 modules.
+- `@risingstack/react-easy-state/dist/cjs.es6.js` exposes an ES6 build with commonJS modules.
+- `@risingstack/react-easy-state/dist/cjs.es5.js` exposes an ES5 build with commonJS modules.
-Page state usually has properties, which belong in the URL and browser history. In this case it is inherently global and it should be handled in a global store. These stores persist between page transitions, but this is perfectly fine. As a bonus you get a faster transition, because you don't always have to re-fetch all of the data. If you do not want data to linger around clean up the relevant parts in `componentWillUnmount`.
+If you use a bundler, set up an alias for `@risingstack/react-easy-state` to point to your desired build. You can learn how to do it with webpack [here](https://webpack.js.org/configuration/resolve/#resolve-alias) and with rollup [here](https://github.com/rollup/rollup-plugin-alias#usage).
-## How does it work?
+## Contributors :sparkles:
-Under the hood Easy State uses the [@nx-js/observer-util](https://github.com/nx-js/observer-util) library, which relies on ES6 Proxies to observe state changes. Thanks to the Proxies it doesn't have edge cases or limitations. You can write any JS code without worrying about the render function. [This blog post](https://blog.risingstack.com/writing-a-javascript-framework-data-binding-es6-proxy/) gives a little sneak peek under the hood of the `observer-util`.
+Contributions are always welcome, please read our [contributing documentation](CONTRIBUTING.md).
-## Contributing
+Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
-Contributions are always welcome. Just send a PR against the master branch or open a new issue. Please make sure that the tests and the linter pass and the coverage remains decent. Thanks!
+
+
+
+
+`;
+
+exports[`TodoMVC App should filter todos: 09. Filter active 1`] = `
+
+
+
+
+ todos
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`TodoMVC App should filter todos: 10. Filter all 1`] = `
+
+
+
+
+ todos
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`TodoMVC App should toggle all todo state at once: 12. Toggle all to completed 1`] = `
+
+
+
+
+ todos
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`TodoMVC App should toggle all todo state at once: 13. Toggle all to active 1`] = `
+
+
+
+
+ todos
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`TodoMVC App should toggle todo status: 05. Toggle Test Todo to completed 1`] = `
+
+
+
+
+ todos
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`TodoMVC App should toggle todo status: 06. Toggle Other Todo to completed 1`] = `
+
+
+
+
+ todos
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`TodoMVC App should toggle todo status: 07. Toggle Test Todo to active 1`] = `
+
+
+
+
+ todos
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/__tests__/autoEffect.no-hook.test.jsx b/__tests__/autoEffect.no-hook.test.jsx
new file mode 100644
index 0000000..921873a
--- /dev/null
+++ b/__tests__/autoEffect.no-hook.test.jsx
@@ -0,0 +1,52 @@
+import React, { Component } from 'react';
+import { render, cleanup } from '@testing-library/react/pure';
+import {
+ view,
+ store,
+ autoEffect,
+ // eslint-disable-next-line import/no-unresolved
+} from '@risingstack/react-easy-state';
+
+describe('AutoEffect edge cases and errors', () => {
+ afterEach(cleanup);
+
+ test(`Using autoEffect in a function component ${
+ process.env.NOHOOK
+ ? 'with a version of react that has no hooks should'
+ : 'should not'
+ } throw an error`, () => {
+ const someEffect = () => {};
+ const person = store({ name: 'Bob' });
+
+ const MyComp = view(() => {
+ autoEffect(() => someEffect(person.name));
+ return
{person.name}
;
+ });
+
+ if (process.env.NOHOOK) {
+ expect(() => render()).toThrow(
+ 'You cannot use autoEffect inside a function component with a pre-hooks version of React. Please update your React version to at least v16.8.0 to use this feature.',
+ );
+ } else {
+ expect(() => render()).not.toThrow();
+ }
+ });
+
+ test('Using autoEffect inside a render of a class component should throw an error', () => {
+ const someEffect = () => {};
+ const person = store({ name: 'Bob' });
+
+ const MyComp = view(
+ class extends Component {
+ render() {
+ autoEffect(() => someEffect(person.name));
+ return
{person.name}
;
+ }
+ },
+ );
+
+ expect(() => render()).toThrow(
+ 'You cannot use autoEffect inside a render of a class component. Please use it in the constructor or lifecycle methods instead.',
+ );
+ });
+});
diff --git a/__tests__/autoEffect.test.jsx b/__tests__/autoEffect.test.jsx
new file mode 100644
index 0000000..ad5b6ab
--- /dev/null
+++ b/__tests__/autoEffect.test.jsx
@@ -0,0 +1,84 @@
+import React from 'react';
+import { render, cleanup } from '@testing-library/react/pure';
+import {
+ view,
+ store,
+ autoEffect,
+ clearEffect,
+ // eslint-disable-next-line import/no-unresolved
+} from '@risingstack/react-easy-state';
+
+describe('autoEffect', () => {
+ afterEach(cleanup);
+
+ test('should auto run global effects', () => {
+ let documentTitle = '';
+ const app = store({ name: 'Online Store' });
+
+ const effect = autoEffect(() => {
+ documentTitle = app.name;
+ });
+ expect(documentTitle).toBe('Online Store');
+
+ app.name = 'Learning Platform';
+ expect(documentTitle).toBe('Learning Platform');
+
+ clearEffect(effect);
+ app.name = 'Social Platform';
+ expect(documentTitle).toBe('Learning Platform');
+ });
+
+ test('should auto run local effects in function components', () => {
+ let documentTitle = '';
+
+ const app = store({ name: 'Online Store' });
+
+ const MyComp = view(() => {
+ autoEffect(() => {
+ documentTitle = app.name;
+ });
+ return
;
+ };
+
+ MyCustomCompName.propTypes = {
+ number: PropTypes.number.isRequired,
+ };
+
+ const ViewComp = view(MyCustomCompName);
+
+ const errorSpy = jest
+ .spyOn(console, 'error')
+ .mockImplementation(message =>
+ expect(message.indexOf('Failed prop type')).not.toBe(-1),
+ );
+ render();
+ expect(errorSpy).toHaveBeenCalled();
+ errorSpy.mockRestore();
+ });
+
+ test('view() should proxy static methods', () => {
+ class Comp extends Component {
+ static getDerivedStateFromError() {}
+
+ static customMethod() {}
+ }
+
+ const ViewComp = view(Comp);
+ expect(ViewComp.getDerivedStateFromError).toBe(
+ Comp.getDerivedStateFromError,
+ );
+ expect(ViewComp.customMethod).toBe(Comp.customMethod);
+ });
+
+ test('view() should proxy static getters', () => {
+ class Comp extends Component {
+ static get defaultProp() {
+ return { key: 'value' };
+ }
+
+ static get customProp() {
+ return { key: 'hello' };
+ }
+ }
+
+ const ViewComp = view(Comp);
+ expect(ViewComp.defaultProps).toEqual(Comp.defaultProps);
+ expect(ViewComp.customProp).toEqual(Comp.customProp);
+ });
+});
diff --git a/__tests__/store.no-hook.test.jsx b/__tests__/store.no-hook.test.jsx
new file mode 100644
index 0000000..aad362f
--- /dev/null
+++ b/__tests__/store.no-hook.test.jsx
@@ -0,0 +1,101 @@
+import React, { Component } from 'react';
+import { render, cleanup } from '@testing-library/react/pure';
+// eslint-disable-next-line import/no-unresolved
+import { view, store } from '@risingstack/react-easy-state';
+
+describe('Store edge cases and errors', () => {
+ afterEach(cleanup);
+
+ test(`Using local state in a function component ${
+ process.env.NOHOOK
+ ? 'with a version of react that has no hooks should'
+ : 'should not'
+ } throw an error`, () => {
+ const MyComp = view(() => {
+ const person = store({ name: 'Bob' });
+ return
{person.name}
;
+ });
+
+ if (process.env.NOHOOK) {
+ expect(() => render()).toThrow(
+ 'You cannot use state inside a function component with a pre-hooks version of React. Please update your React version to at least v16.8.0 to use this feature.',
+ );
+ } else {
+ expect(() => render()).not.toThrow();
+ }
+ });
+
+ test('Using global state in a function component should not throw an error', () => {
+ const person = store({ name: 'Bob' });
+ const MyComp = view(() => {
+ return
{person.name}
;
+ });
+ expect(() => render()).not.toThrow();
+ });
+
+ test('Using global state in a class component should not throw an error', () => {
+ const person = store({ name: 'Bob' });
+ const MyComp = view(
+ class extends Component {
+ render() {
+ return
{person.name}
;
+ }
+ },
+ );
+ expect(() => render()).not.toThrow();
+ });
+
+ test('Using local state in a class component should not throw an error', () => {
+ const MyComp = view(
+ class extends Component {
+ person = store({ name: 'Bob' });
+
+ render() {
+ return
{this.person.name}
;
+ }
+ },
+ );
+
+ expect(() => render()).not.toThrow();
+ });
+
+ test('Using local state inside a render of a class component should throw an error', () => {
+ const MyComp = view(
+ class extends Component {
+ render() {
+ const person = store({ name: 'Bob' });
+ return
{person.name}
;
+ }
+ },
+ );
+
+ expect(() => render()).toThrow(
+ 'You cannot use state inside a render of a class component. Please create your store outside of the render function.',
+ );
+ });
+
+ test('Using function parameter for store should use the returned object', () => {
+ const person = store(() => ({ name: 'Bob' }));
+ const MyComp = view(
+ class extends Component {
+ render() {
+ return
{person.name}
;
+ }
+ },
+ );
+ const { container } = render();
+ expect(container).toHaveTextContent('Bob');
+ });
+
+ if (!process.env.NOHOOK) {
+ test('Using function parameter for store inside function component should use the returned object', () => {
+ const localStore = () => ({ name: 'Bob' });
+ const MyComp = view(() => {
+ const person = store(localStore);
+ return