diff --git a/.all-contributorsrc b/.all-contributorsrc new file mode 100644 index 0000000..1539927 --- /dev/null +++ b/.all-contributorsrc @@ -0,0 +1,33 @@ +{ + "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" + ] + }, + { + "login": "rolandszoke", + "name": "Roland", + "avatar_url": "https://avatars3.githubusercontent.com/u/14181908?v=4", + "profile": "https://github.com/rolandszoke", + "contributions": [ + "code" + ] + } + ], + "contributorsPerLine": 7, + "projectName": "react-easy-state", + "projectOwner": "RisingStack", + "repoType": "github", + "repoHost": "https://github.com", + "skipCi": true +} 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/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f7292f9 --- /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 `lint-tests` or `lint-tests-fix` to lint 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/README.md b/README.md index 08ff201..8e4f6df 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Simple React state management. Made with :heart: and ES6 Proxies. -[![Build](https://img.shields.io/circleci/project/github/RisingStack/react-easy-state/master.svg)](https://circleci.com/gh/RisingStack/react-easy-state/tree/master) [![dependencies Status](https://david-dm.org/RisingStack/react-easy-state/status.svg)](https://david-dm.org/RisingStack/react-easy-state) [![Coverage Status](https://coveralls.io/repos/github/RisingStack/react-easy-state/badge.svg?branch=master&service=github)](https://coveralls.io/github/RisingStack/react-easy-state?branch=master) [![Package size](https://img.shields.io/bundlephobia/minzip/react-easy-state.svg)](https://bundlephobia.com/result?p=react-easy-state) [![Version](https://img.shields.io/npm/v/react-easy-state.svg)](https://www.npmjs.com/package/react-easy-state) [![License](https://img.shields.io/npm/l/react-easy-state.svg)](https://www.npmjs.com/package/react-easy-state) [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=Simple%20React%20state%20management.%20Made%20with%20%E2%9D%A4%EF%B8%8F%20and%20ES6%20Proxies.&url=https://github.com/RisingStack/react-easy-state&hashtags=reactjs,webdev,javascript) +[![Build](https://img.shields.io/circleci/project/github/RisingStack/react-easy-state/master.svg)](https://circleci.com/gh/RisingStack/react-easy-state/tree/master) [![dependencies Status](https://david-dm.org/RisingStack/react-easy-state/status.svg)](https://david-dm.org/RisingStack/react-easy-state) [![Coverage Status](https://coveralls.io/repos/github/RisingStack/react-easy-state/badge.svg?branch=master&service=github)](https://coveralls.io/github/RisingStack/react-easy-state?branch=master) [![Package size](https://img.shields.io/bundlephobia/minzip/react-easy-state.svg)](https://bundlephobia.com/result?p=react-easy-state) [![Version](https://img.shields.io/npm/v/react-easy-state.svg)](https://www.npmjs.com/package/react-easy-state) [![License](https://img.shields.io/npm/l/react-easy-state.svg)](https://www.npmjs.com/package/react-easy-state) [![All Contributors](https://img.shields.io/badge/all_contributors-2-orange.svg?style=flat-square)](#contributors-) Browser support @@ -14,18 +14,19 @@ Simple React state management. Made with :heart: and ES6 Proxies. -- [Introduction](#introduction) -- [Installation](#installation) -- [Usage](#usage) - - [Creating global stores](#creating-global-stores) - - [Creating reactive views](#creating-reactive-views) - - [Creating local stores](#creating-local-stores) -- [Examples with live demos](#examples-with-live-demos) -- [Articles](#articles) -- [Performance](#performance) -- [Platform support](#platform-support) -- [Alternative builds](#alternative-builds) -- [Contributing](#contributing) +* [Introduction](#introduction) +* [Installation](#installation) +* [Usage](#usage) + + [Creating global stores](#creating-global-stores) + + [Creating reactive views](#creating-reactive-views) + + [Creating local stores](#creating-local-stores) +* [Examples with live demos](#examples-with-live-demos) +* [Articles](#articles) +* [Performance](#performance) +* [Platform support](#platform-support) +* [Alternative builds](#alternative-builds) +* [Contributing](#contributing) +* [Contributors ✨](#contributors-%E2%9C%A8) @@ -39,13 +40,15 @@ React Easy State is a practical state management library with two functions and 2. Always wrap your state store objects with `store()`. ```jsx -import React from 'react' -import { store, view } from 'react-easy-state' +import React from 'react'; +import { store, view } from 'react-easy-state'; -const counter = store({ num: 0 }) -const increment = () => counter.num++ +const counter = store({ num: 0 }); +const increment = () => counter.num++; -export default view(() => ) +export default view(() => ( + +)); ``` 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. @@ -80,11 +83,11 @@ _You need npm 5.2+ to use npx._ `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.) ```js -import { store } from 'react-easy-state' +import { store } from 'react-easy-state'; -const user = store({ name: 'Rick' }) +const user = store({ name: 'Rick' }); // stores behave like normal JS objects -user.name = 'Bob' +user.name = 'Bob'; ```
@@ -92,7 +95,7 @@ user.name = 'Bob'

```js -import { store } from 'react-easy-state' +import { store } from 'react-easy-state'; // stores can include any valid JS structure // including nested data, arrays, Maps, Sets, getters, setters, inheritance, ... @@ -101,18 +104,18 @@ const user = store({ firstName: 'Bob', lastName: 'Smith', get name() { - return `${user.firstName} ${user.lastName}` - } + return `${user.firstName} ${user.lastName}`; + }, }, hobbies: ['programming', 'sports'], - friends: new Map() -}) + 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) +user.profile.firstName = 'Bob'; +delete user.profile.lastName; +user.hobbies.push('reading'); +user.friends.set('id', otherUser); ```
@@ -123,16 +126,16 @@ user.friends.set('id', otherUser)

```js -import { store } from 'react-easy-state' +import { store } from 'react-easy-state'; const userStore = store({ user: {}, async fetchUser() { - userStore.user = await fetch('/user') - } -}) + userStore.user = await fetch('/user'); + }, +}); -export default userStore +export default userStore; ``` @@ -145,32 +148,34 @@ export default userStore _userStore.js_ ```js -import { store } from 'react-easy-state' +import { store } from 'react-easy-state'; const userStore = store({ user: {}, async fetchUser() { - userStore.user = await fetch('/user') - } -}) + userStore.user = await fetch('/user'); + }, +}); -export default userStore +export default userStore; ``` _recipesStore.js_ ```js -import { store } from 'react-easy-state' -import userStore from './userStore' +import { store } from 'react-easy-state'; +import userStore from './userStore'; const recipesStore = store({ recipes: [], async fetchRecipes() { - recipesStore.recipes = await fetch(`/recipes?user=${userStore.user.id}`) - } -}) + recipesStore.recipes = await fetch( + `/recipes?user=${userStore.user.id}`, + ); + }, +}); -export default recipesStore +export default recipesStore; ``` @@ -182,18 +187,18 @@ export default recipesStore ```js // DON'T DO THIS -const person = { name: 'Bob' } -person.name = 'Ann' +const person = { name: 'Bob' }; +person.name = 'Ann'; -export default store(person) +export default store(person); ``` ```js // DO THIS INSTEAD -const person = store({ name: 'Bob' }) -person.name = 'Ann' +const person = store({ name: 'Bob' }); +person.name = 'Ann'; -export default person +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. @@ -206,19 +211,21 @@ The first example wouldn't trigger re-renders on the `person.name = 'Ann'` mutat

```jsx -import { store, view } from 'react-easy-state' +import { store, view } from 'react-easy-state'; const counter = store({ num: 0, increment() { // DON'T DO THIS - this.num++ + this.num++; // DO THIS INSTEAD - counter.num++ - } -}) + counter.num++; + }, +}); -export default view(() =>
{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`. @@ -230,19 +237,22 @@ export default view(() =>
{counter.num}
) 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 'react-easy-state' +import React from 'react'; +import { view, store } from 'react-easy-state'; // this is a global state store -const user = store({ name: 'Bob' }) +const user = store({ name: 'Bob' }); // this is re-rendered whenever user.name changes export default view(() => (
- (user.name = ev.target.value)} /> + (user.name = ev.target.value)} + />
Hello {user.name}!
-)) +)); ```
@@ -250,25 +260,25 @@ export default view(() => (

```jsx -import { view, store } from 'react-easy-state' +import { view, store } from 'react-easy-state'; const appStore = store({ - user: { name: 'Ann' } -}) + user: { name: 'Ann' }, +}); const App = view(() => (

My App

-)) +)); // DO THIS -const Profile = view(({ user }) =>

Name: {user.name}

) +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}

+const Profile = ({ user }) =>

Name: {user.name}

; ```
@@ -279,11 +289,11 @@ const Profile = ({ user }) =>

Name: {user.name}

```jsx -import React from 'react' -import { view, store } from 'react-easy-state' +import React from 'react'; +import { view, store } from 'react-easy-state'; -const user = store({ name: 'Bob' }) -const timeline = store({ posts: ['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(() => ( @@ -291,7 +301,7 @@ export default view(() => (
Hello {user.name}!
Your first post is: {timeline.posts[0]}
-)) +)); ``` @@ -313,14 +323,14 @@ export default view(() => (

```jsx -import React from 'react' -import { view, store, batch } from 'react-easy-state' +import React from 'react'; +import { view, store, batch } from 'react-easy-state'; -const user = store({ name: 'Bob', age: 30 }) +const user = store({ name: 'Bob', age: 30 }); function mutateUser() { - user.name = 'Ann' - user.age = 32 + user.name = 'Ann'; + user.age = 32; } // calling `mutateUser` will only trigger a single re-render of the below component @@ -329,31 +339,31 @@ 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 'react-easy-state' +import React from 'react'; +import { view, store, batch } from 'react-easy-state'; -const user = store({ name: 'Bob', age: 30 }) +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 - }) + 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. @@ -366,19 +376,19 @@ export default view(() => (

```jsx -import { view } from 'react-easy-state' -import { withRouter } from 'react-router-dom' -import { withTheme } from 'styled-components' +import { view } from 'react-easy-state'; +import { withRouter } from 'react-router-dom'; +import { withTheme } from 'styled-components'; -const Comp = () =>
A reactive component
+const Comp = () =>
A reactive component
; // DO THIS -withRouter(view(Comp)) -withTheme(view(Comp)) +withRouter(view(Comp)); +withTheme(view(Comp)); // DON'T DO THIS -view(withRouter(Comp)) -view(withTheme(Comp)) +view(withRouter(Comp)); +view(withTheme(Comp)); ``` @@ -404,21 +414,23 @@ This is not necessary if you use React Router 4.4+. You can find more details an 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 'react-easy-state' -import Table from 'rc-table' -import cloneDeep from 'lodash/cloneDeep' +import React from 'react'; +import { view, store } from 'react-easy-state'; +import Table from 'rc-table'; +import cloneDeep from 'lodash/cloneDeep'; const dataStore = store({ items: [ { product: 'Car', - value: 12 - } - ] -}) + value: 12, + }, + ], +}); -export default view(() => ) +export default view(() => ( +
+)); ``` @@ -447,19 +459,22 @@ export default view(() => {

```jsx -import React from 'react' -import { view, store } from 'react-easy-state' +import React from 'react'; +import { view, store } from 'react-easy-state'; export default view(() => { - const [name, setName] = useState('Ann') - const user = store({ age: 30 }) + const [name, setName] = useState('Ann'); + const user = store({ age: 30 }); return (
setName(ev.target.value)} /> - (user.age = ev.target.value)} /> + (user.age = ev.target.value)} + />
- ) -}) + ); +}); ``` @@ -467,19 +482,21 @@ export default view(() => { #### Local stores in class components ```jsx -import React, { Component } from 'react' -import { view, store } from 'react-easy-state' +import React, { Component } from 'react'; +import { view, store } from 'react-easy-state'; class Counter extends Component { - counter = store({ num: 0 }) - increment = () => counter.num++ + counter = store({ num: 0 }); + increment = () => counter.num++; render() { - return + return ( + + ); } } -export default view(Counter) +export default view(Counter); ```
@@ -487,15 +504,15 @@ export default view(Counter)

```jsx -import React, { Component } from 'react' -import { view, store } from 'react-easy-state' +import React, { Component } from 'react'; +import { view, store } from 'react-easy-state'; class Profile extends Component { - state = { name: 'Ann' } - user = store({ age: 30 }) + state = { name: 'Ann' }; + user = store({ age: 30 }); - setName = ev => this.setState({ name: ev.target.value }) - setAge = ev => (this.user.age = ev.target.value) + setName = ev => this.setState({ name: ev.target.value }); + setAge = ev => (this.user.age = ev.target.value); render() { return ( @@ -503,11 +520,11 @@ class Profile extends Component { - ) + ); } } -export default view(Profile) +export default view(Profile); ```
@@ -518,14 +535,14 @@ export default view(Profile)

```jsx -import React, { Component } from 'react' -import { view, store } from 'react-easy-state' +import React, { Component } from 'react'; +import { view, store } from 'react-easy-state'; class Profile extends Component { // DON'T DO THIS - state = store({}) + state = store({}); // DO THIS - user = store({}) + user = store({}); render() {} } ``` @@ -540,22 +557,22 @@ class Profile extends Component { Class components wrapped with `view` have an extra static `deriveStoresFromProps` lifecycle method, which works similarly to the vanilla `getDerivedStateFromProps`. ```jsx -import React, { Component } from 'react' -import { view, store } from 'react-easy-state' +import React, { Component } from 'react'; +import { view, store } from 'react-easy-state'; class NameCard extends Component { - userStore = store({ name: 'Bob' }) + userStore = store({ name: 'Bob' }); static deriveStoresFromProps(props, userStore) { - userStore.name = props.name || userStore.name + userStore.name = props.name || userStore.name; } render() { - return
{this.userStore.name}
+ return
{this.userStore.name}
; } } -export default view(NameCard) +export default view(NameCard); ``` 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. @@ -615,4 +632,25 @@ If you use a bundler, set up an alias for `react-easy-state` to point to your de ## Contributing -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! +Contributions are always welcome. Please read our [contributing documentation](CONTRIBUTING.md). Thanks! + +## Contributors ✨ + +Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): + + + + +
+ + + + +

Miklos Bertalan

💻

Roland Szoke

💻
+ + + + + + +This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!