├── .editorconfig ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .travis.yml ├── .yarnrc ├── CNAME ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── docs ├── .nojekyll ├── CNAME ├── _coverpage.md ├── _sidebar.md ├── advanced.md ├── faq.md ├── get-started.md ├── googlecc31d09edfe4e7cb.html ├── guide.md ├── index.html ├── third-party-libraries.md └── zh-cn │ ├── _coverpage.md │ ├── _sidebar.md │ ├── advanced.md │ ├── faq.md │ ├── get-started.md │ ├── guide.md │ └── third-party-libraries.md ├── gulpfile.js ├── jest.config.js ├── now.json ├── package.json ├── rollup.config.js ├── src ├── __tests__ │ ├── __snapshots__ │ │ └── store.test.tsx.snap │ ├── store.test.tsx │ ├── stores │ │ └── foo.store.ts │ └── utils.ts ├── consumer.tsx ├── container.tsx ├── error.ts ├── executor.tsx ├── index.ts ├── provider.tsx └── store.ts ├── test.html ├── tsconfig.json ├── tslint.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Node CI Test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [8.x, 10.x, 12.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: Install yarn 21 | run: | 22 | curl -o- -L https://yarnpkg.com/install.sh | bash 23 | - name: Yarn install, test, and build 24 | run: | 25 | yarn install 26 | yarn test 27 | yarn build 28 | env: 29 | CI: true 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /lib/ 2 | .DS_Store 3 | 4 | # IDE 5 | .idea/ 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # TypeScript v1 declaration files 46 | typings/ 47 | 48 | # Optional npm cache directory 49 | .npm 50 | 51 | # Optional eslint cache 52 | .eslintcache 53 | 54 | # Optional REPL history 55 | .node_repl_history 56 | 57 | # Output of 'npm pack' 58 | *.tgz 59 | 60 | # Yarn Integrity file 61 | .yarn-integrity 62 | 63 | # dotenv environment variables file 64 | .env 65 | 66 | # next.js build output 67 | .next 68 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "lts/*" 4 | before_install: 5 | - curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.17.3 6 | - export PATH="$HOME/.yarn/bin:$PATH" 7 | install: 8 | - yarn install 9 | - yarn global add codecov codacy-coverage 10 | script: 11 | - yarn test --coverage 12 | - yarn build 13 | after_success: 14 | - codecov 15 | - cat ./coverage/lcov.info | codacy-coverage 16 | cache: 17 | yarn: true 18 | directories: 19 | - "node_modules" 20 | notifications: 21 | email: 22 | on_success: never 23 | on_failure: always 24 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | registry "https://registry.npmjs.org" 2 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | reto.js.org 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at awmleer@sparker.xyz. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 awmleer 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 | # Reto 2 | 3 | [![GitHub](https://img.shields.io/github/license/awmleer/reto.svg?logo=github)](https://github.com/awmleer/reto) 4 | [![codecov](https://img.shields.io/codecov/c/github/awmleer/reto/master?logo=codecov)](https://codecov.io/gh/awmleer/reto) 5 | [![npm version](https://img.shields.io/npm/v/reto.svg?logo=npm)](https://www.npmjs.com/package/reto) 6 | [![npm downloads](https://img.shields.io/npm/dw/reto.svg?logo=npm)](https://www.npmjs.com/package/reto) 7 | [![npm bundle size (minified)](https://img.shields.io/bundlephobia/min/reto.svg?logo=javascript)](https://www.npmjs.com/package/reto) 8 | [![Build Status](https://img.shields.io/travis/awmleer/reto/master?logo=travis-ci)](https://travis-ci.org/awmleer/reto) 9 | [![codacy](https://img.shields.io/codacy/grade/2d15789ec7b1424092ed472f449a0a70?logo=codacy)](https://app.codacy.com/app/awmleer/reto) 10 | [![Vulnerabilities](https://img.shields.io/snyk/vulnerabilities/npm/reto?logo=snyk)](https://app.snyk.io/test/github/awmleer/reto?targetFile=package.json) 11 | ![React](https://img.shields.io/npm/dependency-version/reto/peer/react?logo=react) 12 | 13 | ``` 14 | ___ __ 15 | / _ \___ ___ _____/ /_ 16 | / , _/ -_) / _ `/ __/ __/ 17 | ____ /_/|_|\__/ \_,_/\__/\__/ 18 | / __/ / /____ _______ 19 | _\ \ / __/ _ \ / __/ -_) 20 | /___/ \__/\___/ /_/ \__/ 21 | 22 | ``` 23 | 24 | Flexible and efficient React Store with hooks. 25 | 26 | ## Features 27 | 28 | - Supports all react hooks. Writing a store is just like writing a component. 29 | - Simple but efficient, quite easy to learn. 30 | - Use multiple stores to organize your data. 31 | - Dependency injection based on React Context. 32 | - Strongly typed with Typescript, also works well with JS. 33 | 34 | ## Docs 35 | 36 | [English](https://reto.js.org/#/) | [中文](https://reto.js.org/#/zh-cn/) 37 | 38 | ## Install 39 | 40 | ```bash 41 | $ yarn add reto 42 | # or 43 | $ npm install reto --save 44 | ``` 45 | 46 | ## Try It Online 47 | 48 | [![Edit react](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/awmleer/reto-demo/tree/master/?fontsize=14) 49 | 50 | ## A Simple Example 51 | 52 | Every `Store` is a function similar to a custom hook. In the body of a `Store` function, you can use any react hooks, for example, `useState`, `useEffect`, `useRef`. 53 | 54 | ```jsx 55 | export function FooStore() { 56 | const [x, setX] = useState(initial) 57 | return { 58 | x, 59 | setX 60 | } 61 | } 62 | ``` 63 | 64 | Then, you can provide a store "instance" using `Provider` component. 65 | 66 | ```jsx 67 | import {Provider} from 'reto' 68 | 69 | 70 | 71 | 72 | ``` 73 | 74 | By using the `useStore` hook, you can retrieve the store "instance" in components, and subscribe to its changes. 75 | 76 | ```jsx 77 | import {useStore} from 'reto' 78 | 79 | const App: FC = (props) => { 80 | const fooStore = useStore(FooStore) 81 | 82 | function changeStore() { 83 | fooStore.setX(fooStore.x + 1) 84 | } 85 | return ( 86 |
87 | 88 | {fooStore.x} 89 |
90 | ) 91 | } 92 | ``` 93 | 94 | So when you click the "Change" button, the `setX` function of `fooStore` is executed, thereby triggers the update of state `x` and the rerender of `App` component. Everything is simple and straightforward. 95 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awmleer/reto/c5ae198faead0b1ad1ed66418d62cc7242475026/docs/.nojekyll -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | reto.js.org -------------------------------------------------------------------------------- /docs/_coverpage.md: -------------------------------------------------------------------------------- 1 | # Reto 2 | 3 | > Flexible and efficient React store with hooks. 4 | 5 | - Writing a store is just like writing a custom hook. 6 | - Simple but efficient, learn in 10 minutes. 7 | 8 | [GitHub](https://github.com/awmleer/reto/) 9 | [中文](/zh-cn/) 10 | [Get Started](/get-started) 11 | 12 | ![](https://images.unsplash.com/photo-1529700215145-58542a1f36b6?ixlib=rb-1.2.1&auto=format&fit=crop&w=1949&q=80) 13 | 14 | ![color](#f0f0f0) 15 | -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | * [Get Started](./get-started.md) 2 | * [Guide](./guide.md) 3 | * [Advanced](./advanced.md) 4 | * [FAQ](./faq.md) 5 | * [Third Party Libraries](./third-party-libraries.md) 6 | -------------------------------------------------------------------------------- /docs/advanced.md: -------------------------------------------------------------------------------- 1 | ## Pass Arguments to Store 2 | 3 | You can use the `args` prop of `Provider` to pass arguments to Store. 4 | 5 | ```jsx 6 | export function FooStore(initial = 1) { 7 | const [x, setX] = useState(initial) 8 | return { 9 | x, 10 | setX 11 | } 12 | } 13 | ``` 14 | 15 | ```jsx 16 | 17 | 18 | 19 | ``` 20 | 21 | ## Use in Class Components 22 | 23 | Even though Reto itself is written with hooks, it is still supported to use Reto in class components. You may wonder how to use `useStore` in class components. The answer is: No, you can't. But, there is an substitute for `useStore`, which is `Consumer` component: 24 | 25 | ```jsx 26 | import {Consumer} from 'reto' 27 | 28 | export class App extends Component { 29 | render() { 30 | return ( 31 | 32 | {fooStore => ( 33 | fooStore.x 34 | )} 35 | 36 | ) 37 | } 38 | } 39 | ``` 40 | 41 | 42 | ## How to Solve the Performance Issue 43 | 44 | There are several ways to solve the performance issuse: 45 | 46 | ### Eliminate Redundant Updates of Provider 47 | 48 | When the parent component re-renders, the `Provider` component in it will also re-render, therefore the Store Hook will be re-executed. And finally, all the components subscribed to this Store will be re-rendered. This kind of cascading update will often result in dramatically performance cost. 49 | 50 | ```jsx 51 | function App(props) { 52 | return ( 53 | 54 | {props.children} 55 | 56 | ) 57 | } 58 | ``` 59 | 60 | Fortunately, reto provides the `memo` props for `Provider`, which can be enabled to avoid redundant updates of `Provider`. 61 | 62 | ```jsx 63 | function App(props) { 64 | return ( 65 | 66 | {props.children} 67 | 68 | ) 69 | } 70 | ``` 71 | 72 | When `memo` is turned on, `Provider` will perform a shallow comparison of the old and new `args` arrays. If there is no change, this update will be skipped. 73 | 74 | > In most cases, we recommend turning on `memo`, which is very straightforward and effective for performance optimization. 75 | 76 | ### Partial Subscription 77 | 78 | If a store is too big or it updates too frequently, there may be a performance issue. 79 | 80 | You can pass an additional `deps` function to `useStore` for controlling whether to rerender. 81 | 82 | ```jsx 83 | const fooStore = useStore(FooStore, store => [store.x, store.y[0]]) 84 | ``` 85 | 86 | This is very similar to `useMemo` and `useEffect`. But please notice that the `deps` of `useStore` is **function**. 87 | 88 | ### Split Store 89 | 90 | We recommend splitting a large Store into small parts, so that not only is the code easier to maintain, but performance can also get improved. 91 | 92 | ## Store Ref 93 | 94 | If you want to get the ref of store object, you can use the `storeRef` prop of `Provider`: 95 | 96 | ```jsx 97 | const storeRef = useRef() 98 | function increase() { 99 | storeRef.current.setCount(count + 1) 100 | } 101 | return ( 102 | 103 | {/*...*/} 104 | 105 | ) 106 | ``` 107 | -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | ## Is it another "redux"? 2 | 3 | No. Reto is the replacement for redux and redux-saga. It does the same thing as they do (building app's model layer), but in a more flexible and efficient way. 4 | 5 | ## Can I use Reto in production? 6 | 7 | Yes. Currently we are using Reto in a product at Alibaba. 8 | 9 | ## Will Reto increase my bundle size significantly? 10 | 11 | No. Reto is very very simple and small. 12 | 13 | ## Can I use Reto in a TypeScript project? 14 | 15 | Of course. Reto itself is written in TypeScript. 16 | 17 | ## Can I use Reto with React 16.7 or lower? 18 | 19 | Sorry you can't. Reto is based on the "hooks" feature which is introduced in React 16.8. So if you want to use Reto, you need to upgrade React to 16.8 or higher. 20 | 21 | ## I don't write function components, can I use Reto? 22 | 23 | Yes. Reto is compatible with class components. You can only use hooks in your stores. 24 | -------------------------------------------------------------------------------- /docs/get-started.md: -------------------------------------------------------------------------------- 1 | > **Flexible** and **efficient** React store with hooks. 2 | 3 | ``` 4 | ___ __ 5 | / _ \___ ___ _____/ /_ 6 | / , _/ -_) / _ `/ __/ __/ 7 | ____ /_/|_|\__/ \_,_/\__/\__/ 8 | / __/ / /____ _______ 9 | _\ \ / __/ _ \ / __/ -_) 10 | /___/ \__/\___/ /_/ \__/ 11 | 12 | ``` 13 | 14 | ## Features 15 | 16 | - Supports all React hooks. Writing a store is just like writing a custom hook. 17 | - Simple but efficient, learn in 10 minutes. 18 | - Use multiple stores to organize your data. 19 | - Dependency injection based on React Context. 20 | - Strongly typed with Typescript, also works well with JS. 21 | 22 | ## Try It Online 23 | 24 | 25 | 26 | ## FAQ 27 | 28 | [Here](faq.md) are some frequently asked questions. 29 | 30 | ## Install 31 | 32 | ```bash 33 | $ yarn add reto 34 | # or 35 | $ npm install reto --save 36 | ``` 37 | 38 | ## A Simple Example 39 | 40 | Every `Store` is a function similar to a custom hook. In the body of a `Store` function, you can use any react hooks, for example, `useState`, `useEffect`, `useRef`. 41 | 42 | ```jsx 43 | export function CounterStore() { 44 | const [count, setCount] = useState(1) 45 | 46 | useEffect(() => { 47 | console.log('x is updated.') 48 | }, [count]) 49 | 50 | function increase() { 51 | setCount(count + 1) 52 | } 53 | 54 | return { 55 | count, 56 | increase, 57 | } 58 | } 59 | ``` 60 | 61 | Then, you can provide a store "instance" using the `Provider` component. 62 | 63 | ```jsx 64 | import {Provider} from 'reto' 65 | 66 | 67 | 68 | 69 | ``` 70 | 71 | By using the `useStore` hook, you can retrieve the store "instance" in components, and subscribe to its changes. 72 | 73 | ```jsx 74 | import {useStore} from 'reto' 75 | 76 | const App: FC = (props) => { 77 | const counterStore = useStore(CounterStore) 78 | 79 | return ( 80 |
81 | 82 | {counterStore.count} 83 |
84 | ) 85 | } 86 | ``` 87 | 88 | So when you click the "Change" button, the `setX` function of `fooStore` is executed, thereby triggers the update of state `x` and the rerender of `App` component. Everything is simple and straightforward. 89 | -------------------------------------------------------------------------------- /docs/googlecc31d09edfe4e7cb.html: -------------------------------------------------------------------------------- 1 | google-site-verification: googlecc31d09edfe4e7cb.html -------------------------------------------------------------------------------- /docs/guide.md: -------------------------------------------------------------------------------- 1 | ## Create a Store 2 | 3 | In reto, a Store is essentially a **function**, and we can call any react hooks in it: 4 | 5 | ```jsx 6 | export function CounterStore() { 7 | const [count, setCount] = useState(1) 8 | 9 | useEffect(() => { 10 | console.log('x is updated.') 11 | }, [count]) 12 | 13 | function increase() { 14 | setCount(count + 1) 15 | } 16 | 17 | return { 18 | count, 19 | increase, 20 | } 21 | } 22 | ``` 23 | 24 | Theoretically, store can return a value of any type. However, we recommend **wrap data and operation functions into an object**, and then return this object. 25 | 26 | You'll be quite familiar with the function above if you have written a [custom hook](https://reactjs.org/docs/hooks-custom.html) before. If we change the name of this function into `useCounter`, then it'll just be a custom hook. 27 | 28 | There is another way to understand it: You can regard a store as a special react component. While there are some difference between a store function and a react function component: 1. No props. 2. The return value is not a react node. 29 | 30 | ## Provide a Store Instance 31 | 32 | You can provide a store "instance" using the `Provider` component. 33 | 34 | > To be precise, a store is a function, so there is no "instance". Describing in this way is just for ease of understanding. 35 | 36 | ```jsx 37 | import {Provider} from 'reto' 38 | 39 | 40 | 41 | 42 | ``` 43 | 44 | Every `Provider` component creates a standalone "**sandbox environment**", and execute the store function in this sandbox. 45 | 46 | ## Get the Store Instance 47 | 48 | 49 | By using the `useStore` hook, you can retrieve the store "instance" in components, and **subscribe** to its changes. 50 | 51 | ```jsx 52 | import {useStore} from 'reto' 53 | 54 | const App: FC = (props) => { 55 | const counterStore = useStore(CounterStore) 56 | 57 | return ( 58 |
59 | 60 | {counterStore.count} 61 |
62 | ) 63 | } 64 | ``` 65 | 66 | > Actually, `useStore` is `useContext` under the hood. 67 | 68 | ## Hierarchical Store Tree 69 | 70 | Reto uses React's Context API under the hood. Thereby, you can create a **hierarchical** store tree. 71 | 72 | ``` 73 | ─ Provider of FooStore -> mark as instance A 74 | ├─ Cosumer of FooStore -> we got instance A 75 | └─ Provider of FooStore -> mark as instance B 76 | └─ Consumer of FooStore -> we got instance B 77 | ``` 78 | 79 | Each of the two `Provider`s provides an instance of `FooStore`, and the data are **isolated** completely. 80 | 81 | When a component consumes this Store, it will fetch an instance from the nearest `Provider`. 82 | 83 | ## Dependencies Between Stores 84 | 85 | If you want to "inject" `FooStore` store into `BarStore`, you can call `useStore` in the body of `BarStore`. 86 | 87 | ```jsx 88 | export function BarStore() { 89 | const fooStore = useStore(FooStore) 90 | const [x, setX] = useState(2) 91 | return { 92 | x, 93 | setX, 94 | fooStore, 95 | } 96 | } 97 | ``` 98 | 99 | In the mean while, Reto will know `FooStore` is the dependency of BarStore. So whenever `FooStore` updates, `BarStore` will update too. 100 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Reto - Flexible and efficient React store with hooks. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 16 |
17 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /docs/third-party-libraries.md: -------------------------------------------------------------------------------- 1 | ## useMemo 2 | 3 | If you are familiar with vue's `computed` feature, `useMemo` can help you to solve similar problems. 4 | 5 | ## useAsyncMemo 6 | 7 | [useAsyncMemo](https://github.com/awmleer/use-async-memo) is very suitable for handling asynchronous computed data, for example, fetching paginated data or calling search API when input changes. 8 | 9 | ## withWrapper 10 | 11 | If you create a `Provider` in one component, then you can't get the Store instance with `useStore` in this component: 12 | 13 | ```jsx 14 | const App = function() { 15 | const fooStore = useStore(FooStore) // You can't get the FooStore instance here 16 | return ( 17 | 18 |

Hello.

19 |
20 | ) 21 | } 22 | ``` 23 | 24 | To solve this problem, you can use [withWrapper](https://github.com/awmleer/with-wrapper): 25 | 26 | ```jsx 27 | const App = withWrapper(element => ( 28 | 29 | {element} 30 | 31 | ))(function() { 32 | const fooStore = useStore(FooStore) // Now you can get the FooStore instance 33 | return ( 34 |

Hello.

35 | ) 36 | }) 37 | ``` 38 | 39 | ## immer 40 | 41 | [immer](https://github.com/immerjs/immer) is a tiny package that allows you to work with immutable state in a more convenient way. You can see [here](https://github.com/immerjs/immer#reactsetstate-example) for more information. 42 | 43 | ## useAction 44 | 45 | the function passed to `useEffect` fires after layout and paint, during a deferred event. If you need to execute the effect function immediately, you can use [useAction](https://github.com/awmleer/use-action) in replacement. 46 | 47 | > Unlike componentDidMount and componentDidUpdate, the function passed to useEffect fires after layout and paint, during a deferred event. This makes it suitable for the many common side effects, like setting up subscriptions and event handlers, because most types of work shouldn’t block the browser from updating the screen. 48 | 49 | ## useCreation 50 | 51 | [useCreation](https://github.com/awmleer/use-creation) is the replacement for `useMemo` or `useRef`. 52 | 53 | `useMemo` can't guarantee the memoized value will not be recalculated, while `useCreation` can guarantee that. 54 | 55 | > **You may rely on useMemo as a performance optimization, not as a semantic guarantee.**In the future, React may choose to “forget” some previously memoized values and recalculate them on next render, e.g. to free memory for offscreen components. Write your code so that it still works without `useMemo` — and then add it to optimize performance. 56 | 57 | And similar to `useRef`, you can use `useCreation` to create some constants. But `useCreation` can avoid performance hazards. 58 | 59 | ```javascript 60 | const a = useRef(new Subject()) //A new Subject instance is created in every render. 61 | const b = useCreation(() => new Subject(), []) //By using factory function, Subject is only instantiated once. 62 | ``` 63 | 64 | ## Rx.js 65 | 66 | You can use Rx.js to create a event stream. In order to prevent recreating streams, you can use the [useCreation](https://github.com/awmleer/use-creation) hook. 67 | 68 | ```jsx 69 | export function FooStore() { 70 | const event$ = useCreation(() => new Subject()) // <- Here 71 | 72 | function notify(data) { 73 | event$.next(data) 74 | } 75 | 76 | return { 77 | opened$, 78 | notify, 79 | } 80 | } 81 | ``` 82 | 83 | ## useReducer 84 | 85 | If you like flux or redux, `useReducer` may be suitable for you. But we don't recommend using it. 86 | 87 | -------------------------------------------------------------------------------- /docs/zh-cn/_coverpage.md: -------------------------------------------------------------------------------- 1 | # Reto 2 | 3 | > 基于hooks的React Store,灵活而高效。 4 | 5 | - 写Store就像写Custom Hook 6 | - 简单但高效,10分钟快速上手 7 | 8 | [GitHub](https://github.com/awmleer/reto/) 9 | [English](/) 10 | [快速上手](./get-started.md) 11 | 12 | ![](https://images.unsplash.com/photo-1529700215145-58542a1f36b6?ixlib=rb-1.2.1&auto=format&fit=crop&w=1949&q=80) 13 | 14 | ![color](#f0f0f0) 15 | -------------------------------------------------------------------------------- /docs/zh-cn/_sidebar.md: -------------------------------------------------------------------------------- 1 | * [快速上手](./get-started.md) 2 | * [使用指南](./guide.md) 3 | * [进阶用法](./advanced.md) 4 | * [常见问题](./faq.md) 5 | * [第三方库](./third-party-libraries.md) 6 | -------------------------------------------------------------------------------- /docs/zh-cn/advanced.md: -------------------------------------------------------------------------------- 1 | ## 传递参数给Store 2 | 3 | 可以通过Provider的args属性传递参数给Store: 4 | 5 | ```jsx 6 | export function FooStore(initial = 1) { 7 | const [x, setX] = useState(initial) 8 | return { 9 | x, 10 | setX 11 | } 12 | } 13 | ``` 14 | 15 | ```jsx 16 | 17 | 18 | 19 | ``` 20 | 21 | ## 在类组件中使用 22 | 23 | 虽然Reto自身是通过hooks实现的,但是也是支持在类组件中使用的。显然,我们不能在类组件中使用`useStore`,但是Reto提供了可以在类组件中使用的`Consumer`: 24 | 25 | ```jsx 26 | import {Consumer} from 'reto' 27 | 28 | export class App extends Component { 29 | render() { 30 | return ( 31 | 32 | {fooStore => ( 33 | fooStore.x 34 | )} 35 | 36 | ) 37 | } 38 | } 39 | ``` 40 | 41 | ## 如何解决Store频繁更新所导致的性能问题 42 | 43 | 解决性能问题有以下几种思路: 44 | 45 | ### 避免Provider的冗余更新 46 | 47 | 当父组件重渲染时,其中的`Provider`会跟着重新进行渲染,Store Hook也会被重新执行,最终导致订阅了Store的组件也被重新渲染,这种级联性的更新往往会造成非常大的性能损耗。 48 | 49 | ```jsx 50 | function App(props) { 51 | return ( 52 | 53 | {props.children} 54 | 55 | ) 56 | } 57 | ``` 58 | 59 | 所幸reto为`Provider`提供了`memo`属性,可以避免掉`Provider`的冗余更新。 60 | 61 | ```jsx 62 | function App(props) { 63 | return ( 64 | 65 | {props.children} 66 | 67 | ) 68 | } 69 | ``` 70 | 71 | 当开启`memo`时,`Provider`会对新旧`args`数组进行一次浅比较,如果没有变化,则会跳过这一次的重渲染。 72 | 73 | > 绝大多数情况下,我们都推荐开启`memo`,这对性能的提升非常直接且有效。 74 | 75 | ### 选择性订阅 76 | 77 | `useStore`支持传入一个额外的`deps`函数,来控制是否进行组件的重渲染: 78 | 79 | ```jsx 80 | const fooStore = useStore(FooStore, store => [store.x > 10, store.x < 20]) 81 | ``` 82 | 83 | 这和`useMemo`、`useEffect`的`deps`非常相似,但是,`useStore`的`deps`参数是一个**函数**。 84 | 85 | ### 拆分Store 86 | 87 | 我们建议对一个庞大的Store进行拆分,这样不仅代码更易于维护,性能也会有所改善。 88 | 89 | ## 获取Ref 90 | 91 | 如果你需要获取Store的Ref,可以使用`Provider`的`storeRef`属性: 92 | 93 | ```jsx 94 | const storeRef = useRef() 95 | function increase() { 96 | storeRef.current.setCount(count + 1) 97 | } 98 | return ( 99 | 100 | {/*...*/} 101 | 102 | ) 103 | ``` 104 | -------------------------------------------------------------------------------- /docs/zh-cn/faq.md: -------------------------------------------------------------------------------- 1 | ## Reto是hooks版本的redux么? 2 | 3 | 不是,Reto是redux和redux-saga的替代品。它在工程项目中做的事情和redux一样(构建应用的模型层),但是它所采用的方案却更为灵活高效。 4 | 5 | ## 我可以在生产环境中使用Reto么? 6 | 7 | 是的,笔者所在的一个阿里巴巴的项目组目前就在使用Reto。 8 | 9 | ## Reto会明显增大应用的打包体积么? 10 | 11 | 不会的,Reto非常的简单轻量。 12 | 13 | ## 我可以在TypeScript的项目中使用Reto么? 14 | 15 | 当然可以,Reto本身就是用TypeScript写的。 16 | 17 | ## 我可以在React 16.7及以下的环境中使用Reto么? 18 | 19 | 不可以。。Reto基于React 16.8新发布的"hooks"特性,所以如果你想使用Reto,你需要先将React升级到16.8或者更高。 20 | 21 | ## 我不写函数组件,那我还可以使用Reto么? 22 | 23 | 可以,Reto和类组件是兼容的。你可以只在store中使用hooks。 24 | -------------------------------------------------------------------------------- /docs/zh-cn/get-started.md: -------------------------------------------------------------------------------- 1 | 基于hooks的React Store,灵活而高效。 2 | 3 | ``` 4 | ___ __ 5 | / _ \___ ___ _____/ /_ 6 | / , _/ -_) / _ `/ __/ __/ 7 | ____ /_/|_|\__/ \_,_/\__/\__/ 8 | / __/ / /____ _______ 9 | _\ \ / __/ _ \ / __/ -_) 10 | /___/ \__/\___/ /_/ \__/ 11 | 12 | ``` 13 | 14 | 15 | ## 特性 16 | 17 | - 支持全部的React Hooks,写Store就像写Custom Hook 18 | - 简单但高效,10分钟快速上手 19 | - 从此告别单一状态树,可定义多个store,随用随取 20 | - 基于React Context的依赖注入 21 | - 强类型支持,但同时兼容js环境 22 | 23 | ## 在线体验 24 | 25 | 26 | 27 | ## 常见问题 28 | 29 | [这里](https://)是一些常见的问题。 30 | 31 | ## 安装 32 | 33 | ```bash 34 | $ yarn add reto 35 | # or 36 | $ npm install reto --save 37 | ``` 38 | 39 | ## 一个简单的例子 40 | 41 | 每一个`Store`其实就是一个类似于custom hook的函数。在`Store`的函数体中,你可以随意使用react hooks,例如`useState`、`useEffect`、`useRef`。 42 | 43 | ```jsx 44 | export function CounterStore() { 45 | const [count, setCount] = useState(1) 46 | 47 | useEffect(() => { 48 | console.log('x is updated.') 49 | }, [count]) 50 | 51 | function increase() { 52 | setCount(count + 1) 53 | } 54 | 55 | return { 56 | count, 57 | increase, 58 | } 59 | } 60 | ``` 61 | 62 | 通过`Provider`组件提供一个`FooStore`的"实例"。 63 | 64 | ```jsx 65 | import {Provider} from 'reto' 66 | 67 | 68 | 69 | 70 | ``` 71 | 72 | 在组件中通过`useStore`获取并订阅`FooStore`的更新。 73 | 74 | ```jsx 75 | import {useStore} from 'reto' 76 | 77 | const App: FC = (props) => { 78 | const counterStore = useStore(CounterStore) 79 | 80 | return ( 81 |
82 | 83 | {counterStore.count} 84 |
85 | ) 86 | } 87 | ``` 88 | 89 | 当点击按钮时,会调用`fooStore`中的`setX`函数,从而触发`x`的更新以及`App`组件的重渲染,一切都非常简单而自然。 90 | -------------------------------------------------------------------------------- /docs/zh-cn/guide.md: -------------------------------------------------------------------------------- 1 | ## 创建一个Store 2 | 3 | 在reto中,Store本质上就是**函数**,在这个函数中,我们可以调用任何react hooks: 4 | 5 | ```jsx 6 | export function CounterStore() { 7 | const [count, setCount] = useState(1) 8 | 9 | useEffect(() => { 10 | console.log('x is updated.') 11 | }, [count]) 12 | 13 | function increase() { 14 | setCount(count + 1) 15 | } 16 | 17 | return { 18 | count, 19 | increase, 20 | } 21 | } 22 | ``` 23 | 24 | 理论上,Store的返回值可以是任何类型,但通常,我们建议**把数据和操作函数包装成一个object再进行返回**。 25 | 26 | 如果你写过[custom hook](https://reactjs.org/docs/hooks-custom.html),那么上面这个函数你一定不会感到陌生。如果把函数的名字改为`useCounter`,它就变成了一个不折不扣的custom hook。 27 | 28 | 另一种理解是,把store视作成一个特殊的react函数组件,它和react函数组件的区别主要是:1. 没有props 2. 返回值不是react node。 29 | 30 | ## 提供一个Store的实例 31 | 32 | 使用`Provider`组件可以创建并提供一个Store的实例。 33 | 34 | > 准确的讲,Store是函数,是没有所谓的"实例"概念的,这里这样表述只是为了便于理解。 35 | 36 | ```jsx 37 | import {Provider} from 'reto' 38 | 39 | 40 | 41 | 42 | ``` 43 | 44 | 每一个`Provider`组件都会创建出一个独立的"**沙箱环境**",并在这个"沙箱"中执行Store函数。 45 | 46 | ## 获取一个Store的实例 47 | 48 | 使用`useStore`可以在组件中获取到Store的实例,并且同时**订阅更新**。 49 | 50 | ```jsx 51 | import {useStore} from 'reto' 52 | 53 | const App: FC = (props) => { 54 | const counterStore = useStore(CounterStore) 55 | 56 | return ( 57 |
58 | 59 | {counterStore.count} 60 |
61 | ) 62 | } 63 | ``` 64 | 65 | > `useStore`本质上就是react的`useContext`。 66 | 67 | ## 多层级的Store树 68 | 69 | Reto在底层使用的是React的context API,因此可以创建出**多层级**的Store树。 70 | 71 | ``` 72 | ─ Provider of FooStore -> 记为实例A 73 | ├─ Cosumer of FooStore -> 拿到的是实例A 74 | └─ Provider of FooStore -> 记为实例B 75 | └─ Consumer of FooStore -> 拿到的是实例B 76 | ``` 77 | 78 | 虽然两个`Provider`都是提供的`FooStore`,但是他们之间的数据是彼此完全**隔离**的。 79 | 80 | 而组件在消费Store时,会在组件树中**就近获取**实例。 81 | 82 | ## Store之间的依赖 83 | 84 | 你可以在一个Store的函数体中使用`useStore`来引入另一个Store: 85 | 86 | ```jsx 87 | export function BarStore() { 88 | const fooStore = useStore(FooStore) 89 | const [x, setX] = useState(2) 90 | return { 91 | x, 92 | setX, 93 | fooStore, 94 | } 95 | } 96 | ``` 97 | 98 | 同时,Reto会将其作为本Store的**依赖**。因此,当`FooStore`更新时,`BarStore`也会自动跟着更新。 99 | -------------------------------------------------------------------------------- /docs/zh-cn/third-party-libraries.md: -------------------------------------------------------------------------------- 1 | ## useMemo 2 | 3 | 如果你熟悉vue的`computed`特性,那么`useMemo`可以帮助你解决类似的问题。 4 | 5 | ## useAsyncMemo 6 | 7 | [useAsyncMemo](https://github.com/awmleer/use-async-memo)非常适合用来处理异步的计算数据,它对于`useMemo`是一个很好的补充。例如在翻页(`pageNumber`变化)时调用API加载远程数据。 8 | 9 | ## withWrapper 10 | 11 | 如果你在一个组件中创建了一个`FooStore`的`Provider`,那么是无法同时在这个组件通过`useStore(FooStore)`来获取这个Store的实例的: 12 | 13 | ```jsx 14 | const App = function() { 15 | const fooStore = useStore(FooStore) // 这里是获取不到 FooStore 实例的 16 | return ( 17 | 18 |

Hello.

19 |
20 | ) 21 | } 22 | ``` 23 | 24 | 为了解决这个问题,可以使用[withWrapper](https://github.com/awmleer/with-wrapper): 25 | 26 | ```jsx 27 | const App = withWrapper(element => ( 28 | 29 | {element} 30 | 31 | ))(function() { 32 | const fooStore = useStore(FooStore) // 现在这里可以获取到 FooStore 实例了 33 | return ( 34 |

Hello.

35 | ) 36 | }) 37 | ``` 38 | 39 | ## immer 40 | 41 | 使用[immer](https://github.com/immerjs/immer)可以在某些时候简化`setState`的语法,详细用法可以参考其官方文档上的[介绍](https://github.com/immerjs/immer#reactsetstate-example)。 42 | 43 | ## useAction 44 | 45 | 由于`useEffect`是会延后执行的,因此在Reto中,[useAction](https://github.com/awmleer/use-action)是一个更适合的替代品。 46 | 47 | > Unlike componentDidMount and componentDidUpdate, the function passed to useEffect fires after layout and paint, during a deferred event. This makes it suitable for the many common side effects, like setting up subscriptions and event handlers, because most types of work shouldn’t block the browser from updating the screen. 48 | 49 | ## useCreation 50 | 51 | [useCreation](https://github.com/awmleer/use-creation)是`useMemo`或`useRef`的替代品。 52 | 53 | 因为`useMemo`不能保证被memo的值一定不会被重计算,而`useCreation`可以保证这一点。 54 | 55 | > **You may rely on useMemo as a performance optimization, not as a semantic guarantee.**In the future, React may choose to “forget” some previously memoized values and recalculate them on next render, e.g. to free memory for offscreen components. Write your code so that it still works without `useMemo` — and then add it to optimize performance. 56 | 57 | 而相比于`useRef`,你可以使用`useCreation`创建一些常量,这些常量和`useRef`创建出来的ref有很多使用场景上的相似,但对于复杂常量的创建,`useRef`却容易出现潜在的性能隐患。 58 | 59 | ```javascript 60 | const a = useRef(new Subject()) //每次重渲染,都会执行实例化Subject的过程,即便这个实例立刻就被扔掉了 61 | const b = useCreation(() => new Subject(), []) //通过factory函数,可以避免性能隐患 62 | ``` 63 | 64 | ## Rx.js 65 | 66 | Rx.js非常适合事件通知的逻辑,在Reto中,可以配合`useCreation`使用Rx.js: 67 | 68 | ```jsx 69 | export function FooStore() { 70 | const event$ = useCreation(() => new Subject()) // 使用useCreation来避免重复创建Subject 71 | 72 | function notify(data) { 73 | event$.next(data) 74 | } 75 | 76 | return { 77 | opened$, 78 | notify, 79 | } 80 | } 81 | ``` 82 | 83 | ## useReducer 84 | 85 | 当Store变得非常复杂时,可以通过`useReducer`更好的组织逻辑。详见[文档](https://reactjs.org/docs/hooks-reference.html#usereducer)。不过笔者并不非常推荐使用它,因为当组件变得过于庞大时,更好的办法是进行拆分。 86 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp') 2 | const del = require('del') 3 | const rollupConfig = require('./rollup.config') 4 | const rollup = require('rollup') 5 | const ts = require('gulp-typescript') 6 | 7 | gulp.task('clean-lib', async function() { 8 | await del('lib/**') 9 | }) 10 | 11 | gulp.task('copy-files', function() { 12 | return gulp 13 | .src(['package.json', 'README.md', 'LICENSE']) 14 | .pipe(gulp.dest('lib/')) 15 | }) 16 | 17 | gulp.task('ts', function() { 18 | const tsProject = ts.createProject('tsconfig.json') 19 | return tsProject.src() 20 | .pipe(tsProject()) 21 | .pipe(gulp.dest('lib')) 22 | }) 23 | 24 | gulp.task('rollup', gulp.series( 25 | function() { 26 | const tsProject = ts.createProject('tsconfig.json', { 27 | module: 'esnext', 28 | }) 29 | return tsProject.src() 30 | .pipe(tsProject()) 31 | .pipe(gulp.dest('lib/es')) 32 | }, 33 | async function() { 34 | const bundle = await rollup.rollup(rollupConfig) 35 | return await bundle.write(rollupConfig.output) 36 | }, 37 | async function() { 38 | await del('lib/es') 39 | } 40 | )) 41 | 42 | gulp.task('build', gulp.series('clean-lib', 'rollup', 'ts', 'copy-files')) 43 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | setupFilesAfterEnv: [ 6 | '@testing-library/react/cleanup-after-each', 7 | ], 8 | // All imported modules in your tests should be mocked automatically 9 | // automock: false, 10 | 11 | // Stop running tests after the first failure 12 | // bail: false, 13 | 14 | // Respect "browser" field in package.json when resolving modules 15 | // browser: false, 16 | 17 | // The directory where Jest should state its cached dependency information 18 | // cacheDirectory: "/var/folders/bx/hmqz3c7x2nb8pknt576dv7bw0000gn/T/jest_dx", 19 | 20 | // Automatically clear mock calls and instances between every test 21 | // clearMocks: false, 22 | 23 | // Indicates whether the coverage information should be collected while executing the test 24 | // collectCoverage: false, 25 | 26 | // An array of glob patterns indicating a set of files for which coverage information should be collected 27 | collectCoverageFrom: [ 28 | "/src/**" 29 | ], 30 | 31 | // The directory where Jest should output its coverage files 32 | coverageDirectory: "coverage", 33 | 34 | // An array of regexp pattern strings used to skip coverage collection 35 | coveragePathIgnorePatterns: [ 36 | "/node_modules/", 37 | "/__tests__/", 38 | ], 39 | 40 | // A list of reporter names that Jest uses when writing coverage reports 41 | // coverageReporters: [ 42 | // "json", 43 | // "text", 44 | // "lcov", 45 | // "clover" 46 | // ], 47 | 48 | // An object that configures minimum threshold enforcement for coverage results 49 | // coverageThreshold: null, 50 | 51 | // Make calling deprecated APIs throw helpful error messages 52 | // errorOnDeprecated: false, 53 | 54 | // Force coverage collection from ignored files usin a array of glob patterns 55 | // forceCoverageMatch: [], 56 | 57 | // A path to a module which exports an async function that is triggered once before all test suites 58 | // globalSetup: null, 59 | 60 | // A path to a module which exports an async function that is triggered once after all test suites 61 | // globalTeardown: null, 62 | 63 | // A set of global variables that need to be available in all test environments 64 | globals: { 65 | 'ts-jest': { 66 | tsConfig: 'tsconfig.json', 67 | }, 68 | }, 69 | 70 | // An array of directory names to be searched recursively up from the requiring module's location 71 | // moduleDirectories: [ 72 | // "node_modules" 73 | // ], 74 | 75 | // An array of file extensions your modules use 76 | moduleFileExtensions: [ 77 | "ts", 78 | "tsx", 79 | "js" 80 | ], 81 | 82 | // A map from regular expressions to module names that allow to stub out resources with a single module 83 | // moduleNameMapper: {}, 84 | 85 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 86 | // modulePathIgnorePatterns: [], 87 | 88 | // Activates notifications for test results 89 | // notify: false, 90 | 91 | // An enum that specifies notification mode. Requires { notify: true } 92 | // notifyMode: "always", 93 | 94 | // A preset that is used as a base for Jest's configuration 95 | preset: 'ts-jest', 96 | 97 | // Run tests from one or more projects 98 | // projects: null, 99 | 100 | // Use this configuration option to add custom reporters to Jest 101 | // reporters: undefined, 102 | 103 | // Automatically reset mock state between every test 104 | // resetMocks: false, 105 | 106 | // Reset the module registry before running each individual test 107 | // resetModules: false, 108 | 109 | // A path to a custom resolver 110 | // resolver: null, 111 | 112 | // Automatically restore mock state between every test 113 | // restoreMocks: false, 114 | 115 | // The root directory that Jest should scan for tests and modules within 116 | // rootDir: null, 117 | 118 | // A list of paths to directories that Jest should use to search for files in 119 | // roots: [ 120 | // "" 121 | // ], 122 | 123 | // Allows you to use a custom runner instead of Jest's default test runner 124 | // runner: "jest-runner", 125 | 126 | // The paths to modules that run some code to configure or set up the testing environment before each test 127 | // setupFiles: [], 128 | 129 | // The path to a module that runs some code to configure or set up the testing framework before each test 130 | // setupTestFrameworkScriptFile: null, 131 | 132 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 133 | // snapshotSerializers: [], 134 | 135 | // The test environment that will be used for testing 136 | // testEnvironment: "jest-environment-jsdom", 137 | 138 | // Options that will be passed to the testEnvironment 139 | // testEnvironmentOptions: {}, 140 | 141 | // Adds a location field to test results 142 | // testLocationInResults: false, 143 | 144 | // The glob patterns Jest uses to detect test files 145 | testMatch: [ 146 | "**/__tests__/*.test.+(ts|tsx|js)" 147 | ], 148 | 149 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 150 | // testPathIgnorePatterns: [ 151 | // "/node_modules/" 152 | // ], 153 | 154 | // The regexp pattern Jest uses to detect test files 155 | // testRegex: "", 156 | 157 | // This option allows the use of a custom results processor 158 | // testResultsProcessor: null, 159 | 160 | // This option allows use of a custom test runner 161 | // testRunner: "jasmine2", 162 | 163 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 164 | // testURL: "http://localhost", 165 | 166 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 167 | // timers: "real", 168 | 169 | // A map from regular expressions to paths to transformers 170 | transform: { 171 | "^.+\\.(ts|tsx)$": "ts-jest" 172 | }, 173 | 174 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 175 | // transformIgnorePatterns: [ 176 | // "/node_modules/" 177 | // ], 178 | 179 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 180 | // unmockedModulePathPatterns: undefined, 181 | 182 | // Indicates whether each individual test should be reported during the run 183 | // verbose: null, 184 | 185 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 186 | // watchPathIgnorePatterns: [], 187 | 188 | // Whether to use watchman for file crawling 189 | // watchman: true, 190 | }; 191 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "scope": "awmleer", 3 | "builds": [ 4 | { 5 | "src": "package.json", 6 | "use": "@now/static-build", 7 | "config": { "distDir": "docs" } 8 | } 9 | ], 10 | "github": { 11 | "silent": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reto", 3 | "version": "0.9.3", 4 | "main": "index.js", 5 | "repository": "https://github.com/awmleer/reto", 6 | "description": "React store with hooks.", 7 | "author": "awmleer ", 8 | "license": "MIT", 9 | "private": false, 10 | "peerDependencies": { 11 | "react": "^16.8 || ^17.0" 12 | }, 13 | "scripts": { 14 | "build": "gulp build", 15 | "test": "jest", 16 | "lint": "tslint -p tsconfig.json", 17 | "umd": "rollup --config rollup.config.js" 18 | }, 19 | "devDependencies": { 20 | "@testing-library/jest-dom": "^4.0.0", 21 | "@testing-library/react": "^8.0.7", 22 | "@types/jest": "^24.0.18", 23 | "@types/node": "^10.12.24", 24 | "@types/react": "^16.8.0", 25 | "@types/react-dom": "^16.8.0", 26 | "@types/react-test-renderer": "^16.8.0", 27 | "del": "^5.0.0", 28 | "gulp": "^4.0.2", 29 | "gulp-typescript": "^5.0.1", 30 | "jest": "^24.1.0", 31 | "react": "^16.8.0", 32 | "react-dom": "^16.8.0", 33 | "react-test-renderer": "^16.8.0", 34 | "rollup": "^1.21.4", 35 | "ts-jest": "^24.0.2", 36 | "tslint": "^5.18.0", 37 | "typescript": "^3.4.5" 38 | }, 39 | "dependencies": {} 40 | } 41 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | external: [ 3 | 'react', 4 | ], 5 | input: 'lib/es/index.js', 6 | output: { 7 | name: 'reto', 8 | globals: { 9 | react: 'React' 10 | }, 11 | file: 'lib/umd/reto.js', 12 | format: 'umd' 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/store.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Consumer 1`] = ` 4 | 5 | 1 6 | 7 | `; 8 | 9 | exports[`handle return undefined from state function 1`] = ` 10 | 11 |
12 | content 13 |
14 |
15 | `; 16 | 17 | exports[`no extra render on children 1`] = ` 18 | 19 |
20 | 23 | parent state: 1 24 |
25 | 28 | 1 29 |
30 |
31 |
32 | `; 33 | 34 | exports[`no extra render on children 2`] = ` 35 | 36 |
37 | 40 | parent state: 1 41 |
42 | 45 | 2 46 |
47 |
48 |
49 | `; 50 | 51 | exports[`no extra render on children 3`] = ` 52 | 53 |
54 | 57 | parent state: 2 58 |
59 | 62 | 2 63 |
64 |
65 |
66 | `; 67 | 68 | exports[`no extra render on children 4`] = ` 69 | 70 |
71 | 74 | parent state: 3 75 |
76 | 79 | 2 80 |
81 |
82 |
83 | `; 84 | 85 | exports[`provider initialize 1`] = ` 86 | 87 |
88 | 91 | 1 92 |
93 |
94 | `; 95 | 96 | exports[`provider initialize 2`] = ` 97 | 98 |
99 | 102 | 2 103 |
104 |
105 | `; 106 | 107 | exports[`provider initialize with args 1`] = ` 108 | 109 |
110 | 113 | 5 114 |
115 |
116 | `; 117 | 118 | exports[`provider initialize with args 2`] = ` 119 | 120 |
121 | 124 | 6 125 |
126 |
127 | `; 128 | 129 | exports[`provider memo 1`] = ` 130 | 131 |

132 | 1 133 |

134 |

135 | 1 136 |

137 |

138 | 1 139 |

140 |
141 | `; 142 | 143 | exports[`provider memo 2`] = ` 144 | 145 |

146 | 2 147 |

148 |

149 | 2 150 |

151 |

152 | 1 153 |

154 |
155 | `; 156 | 157 | exports[`rerender on dependency update 1`] = ` 158 | 159 |
160 | 163 | 1 164 |
165 |
166 | `; 167 | 168 | exports[`rerender on dependency update 2`] = ` 169 | 170 |
171 | 174 | 3 175 |
176 |
177 | `; 178 | 179 | exports[`storeRef 1`] = ` 180 | Object { 181 | "current": null, 182 | } 183 | `; 184 | 185 | exports[`storeRef 2`] = ` 186 | Object { 187 | "current": Object { 188 | "setX": [Function], 189 | "x": 1, 190 | }, 191 | } 192 | `; 193 | 194 | exports[`useStoreOptionally 1`] = ` 195 | 196 |
197 | false 198 |
199 |
200 | `; 201 | -------------------------------------------------------------------------------- /src/__tests__/store.test.tsx: -------------------------------------------------------------------------------- 1 | import {act} from '@testing-library/react' 2 | import {Consumer, Provider, useStore, useStoreOptionally} from '..' 3 | import {BarStore, FooStore} from './stores/foo.store' 4 | import * as React from 'react' 5 | import {createRef, FC, forwardRef, memo, useImperativeHandle, useRef, useState} from 'react' 6 | import * as testing from '@testing-library/react' 7 | 8 | 9 | // let originalConsole: typeof global.console 10 | // beforeEach(() => { 11 | // const mocked = jest.fn() 12 | // originalConsole = global.console 13 | // global.console = { 14 | // ...originalConsole, 15 | // error: mocked 16 | // } 17 | // }) 18 | // 19 | // afterEach(() => { 20 | // global.console = originalConsole 21 | // }) 22 | 23 | 24 | test('provider initialize', function () { 25 | const App: FC = (props) => { 26 | const fooStore = useStore(FooStore) 27 | 28 | function changeStore() { 29 | fooStore.setX(fooStore.x + 1) 30 | } 31 | return ( 32 |
33 | 34 | {fooStore.x} 35 |
36 | ) 37 | } 38 | const renderer = testing.render( 39 | 40 | 41 | 42 | ) 43 | expect(renderer.asFragment()).toMatchSnapshot() 44 | testing.fireEvent.click(testing.getByText(renderer.container, 'Change')) 45 | expect(renderer.asFragment()).toMatchSnapshot() 46 | }) 47 | 48 | test('Consumer', function () { 49 | const test = 1 50 | const renderer = testing.render( 51 | 52 | 53 | {fooStore => ( 54 | fooStore.x 55 | )} 56 | 57 | 58 | ) 59 | expect(renderer.asFragment()).toMatchSnapshot() 60 | }) 61 | 62 | 63 | test('provider initialize with args', function () { 64 | const App: FC = (props) => { 65 | const fooStore = useStore(FooStore) 66 | 67 | function changeStore() { 68 | fooStore.setX(fooStore.x + 1) 69 | } 70 | return ( 71 |
72 | 73 | {fooStore.x} 74 |
75 | ) 76 | } 77 | const renderer = testing.render( 78 | 79 | 80 | 81 | ) 82 | expect(renderer.asFragment()).toMatchSnapshot() 83 | testing.fireEvent.click(testing.getByText(renderer.container, 'Change')) 84 | expect(renderer.asFragment()).toMatchSnapshot() 85 | }) 86 | 87 | 88 | test('no extra render on children', function () { 89 | const renderCount = { 90 | a: 0, 91 | b: 0, 92 | } 93 | 94 | const Parent: FC = props => { 95 | const [state, setState] = useState(1) 96 | return ( 97 |
98 | 99 | parent state: {state} 100 | 101 | 102 | 103 | 104 |
105 | ) 106 | } 107 | 108 | function Test() { 109 | const Component: FC = (props) =>
{props.children}
110 | return Component 111 | } 112 | 113 | const ChildA: FC = memo((props) => { 114 | renderCount.a++ 115 | const fooStore = useStore(FooStore, (store) => [store.x]) 116 | function changeStore() { 117 | fooStore.setX(fooStore.x + 1) 118 | } 119 | return ( 120 |
121 | 122 | {fooStore.x} 123 |
124 | ) 125 | }) 126 | 127 | const ChildB: FC = (props) => { 128 | renderCount.b++ 129 | return null 130 | } 131 | 132 | const renderer = testing.render( 133 | 134 | ) 135 | 136 | expect(renderer.asFragment()).toMatchSnapshot() 137 | testing.fireEvent.click(testing.getByText(renderer.container, 'Change Store')) 138 | expect(renderer.asFragment()).toMatchSnapshot() 139 | testing.fireEvent.click(testing.getByText(renderer.container, 'Change Parent')) 140 | expect(renderer.asFragment()).toMatchSnapshot() 141 | testing.fireEvent.click(testing.getByText(renderer.container, 'Change Parent')) 142 | expect(renderer.asFragment()).toMatchSnapshot() 143 | expect(renderCount.a).toBe(2) 144 | expect(renderCount.b).toBe(3) 145 | }) 146 | 147 | 148 | test('rerender on dependency update', function () { 149 | const App: FC = (props) => { 150 | const barStore = useStore(BarStore) 151 | 152 | function changeStore() { 153 | barStore.fooStore.setX(3) 154 | } 155 | return ( 156 |
157 | 158 | {barStore.fooStore.x} 159 |
160 | ) 161 | } 162 | const renderer = testing.render( 163 | 164 | 165 | 166 | 167 | 168 | ) 169 | expect(renderer.asFragment()).toMatchSnapshot() 170 | testing.fireEvent.click(testing.getByText(renderer.container, 'Change')) 171 | expect(renderer.asFragment()).toMatchSnapshot() 172 | }) 173 | 174 | 175 | test('handle return undefined from state function', () => { 176 | function FooStore() {} 177 | const App = function() { 178 | const fooStore = useStore(FooStore) 179 | return
content
180 | } 181 | const renderer = testing.render( 182 | 183 | 184 | 185 | ) 186 | expect(renderer.asFragment()).toMatchSnapshot() 187 | }) 188 | 189 | 190 | test('useStoreOptionally', () => { 191 | const FooStore = function() { 192 | const [x, setX] = useState(1) 193 | return {x, setX} 194 | } 195 | function App() { 196 | const [fooStore, fooStoreExist] = useStoreOptionally(FooStore) 197 | return ( 198 |
{fooStoreExist.toString()}
199 | ) 200 | } 201 | const renderer = testing.render( 202 | 203 | ) 204 | expect(renderer.asFragment()).toMatchSnapshot() 205 | }) 206 | 207 | 208 | test('provider memo', () => { 209 | const FooStore = function() { 210 | const ref = useRef(0) 211 | ref.current++ 212 | return { 213 | x: ref.current 214 | } 215 | } 216 | 217 | function Show() { 218 | const fooStore = useStore(FooStore) 219 | return ( 220 |

{fooStore.x}

221 | ) 222 | } 223 | 224 | const App = forwardRef((props, ref) => { 225 | const [flag, setFlag] = useState(0) 226 | useImperativeHandle(ref,() => { 227 | return { 228 | setFlag 229 | } 230 | }) 231 | return ( 232 | <> 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | ) 244 | } 245 | ) 246 | const appRef = createRef() 247 | const renderer = testing.render( 248 | 249 | ) 250 | expect(renderer.asFragment()).toMatchSnapshot() 251 | act(() => { 252 | appRef.current.setFlag(1) 253 | }) 254 | expect(renderer.asFragment()).toMatchSnapshot() 255 | }) 256 | 257 | 258 | test('storeRef', () => { 259 | const FooStore = function() { 260 | const [x, setX] = useState(1) 261 | return {x, setX} 262 | } 263 | const ref = createRef>() 264 | expect(ref).toMatchSnapshot() 265 | testing.render( 266 | 267 | ) 268 | expect(ref).toMatchSnapshot() 269 | }) 270 | 271 | 272 | // test('ref', function () { 273 | // function FooStore() { 274 | // return 'foo' 275 | // } 276 | // const storeRef = React.createRef>() 277 | // const App = withProvider({ 278 | // of: FooStore, 279 | // ref: storeRef 280 | // })(function App() { 281 | // const fooStore = useStore(FooStore) 282 | // return null 283 | // }) 284 | // const renderer = testing.render( 285 | // 286 | // ) 287 | // expect(storeRef.current).toBe('foo') 288 | // }) 289 | 290 | // test('handle no context', function() { 291 | // const originalError = console.error 292 | // console.error = jest.fn() 293 | // 294 | // global.console = { 295 | // ...global.console, 296 | // error: () => { 297 | // console.log(1111111111) 298 | // } 299 | // } 300 | // 301 | // const App: FC = () => { 302 | // const fooStore = useStore(FooStore) 303 | // return null 304 | // } 305 | // const renderer = testing.render( 306 | // 307 | // ) 308 | // 309 | // expect(console.error).toHaveBeenCalled() 310 | // 311 | // console.error = originalError 312 | // }) 313 | -------------------------------------------------------------------------------- /src/__tests__/stores/foo.store.ts: -------------------------------------------------------------------------------- 1 | import {useState} from 'react' 2 | import {useStore} from '../..' 3 | 4 | export function FooStore(initial = 1) { 5 | const [x, setX] = useState(initial) 6 | return { 7 | x, 8 | setX 9 | } 10 | } 11 | 12 | export function BarStore() { 13 | const fooStore = useStore(FooStore) 14 | const [x, setX] = useState(2) 15 | return { 16 | x, 17 | setX, 18 | fooStore, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/__tests__/utils.ts: -------------------------------------------------------------------------------- 1 | export const sleep = (time: number) => new Promise(resolve => setTimeout(resolve, time)) 2 | -------------------------------------------------------------------------------- /src/consumer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {ReactNode, useContext, useEffect, useRef, useState} from 'react' 3 | import {ReactElement} from 'react' 4 | import {Container} from './container' 5 | import {NoStoreError} from './error' 6 | import {defaultStoreValue, getStoreContext, Store, StoreV} from './store' 7 | 8 | const useDebugValue = React.useDebugValue ?? (() => {}) 9 | 10 | type Deps = (store: T) => unknown[] 11 | 12 | interface Props { 13 | of: S 14 | deps?: Deps> 15 | children: (store: StoreV) => ReactNode 16 | } 17 | 18 | export function Consumer(props: Props) { 19 | const store = useStore(props.of) 20 | return props.children(store) as ReactElement 21 | } 22 | 23 | export function useStore(s: S, deps?: Deps>) { 24 | const name = s.displayName || s.name 25 | useDebugValue(name) 26 | const Context = getStoreContext(s) 27 | const container = useContext(Context) as Container> 28 | 29 | const [state, setState] = useState>(container.state) 30 | 31 | const depsRef = useRef([]) 32 | 33 | useEffect(() => { 34 | const subscriber = () => { 35 | if (!deps) { 36 | setState(container.state) 37 | } else { 38 | const oldDeps = depsRef.current 39 | const newDeps = deps(container.state) 40 | if (compare(oldDeps, newDeps)) { 41 | setState(container.state) 42 | } 43 | depsRef.current = newDeps 44 | } 45 | } 46 | container.subscribers.add(subscriber) 47 | return () => { 48 | container.subscribers.delete(subscriber) 49 | } 50 | }, []) 51 | 52 | if (state === defaultStoreValue) { 53 | throw new NoStoreError(`No store context of "${name}" found. And "${name}" doesn't have defaultValue. Either render a Provider or set a defaultValue for this Store.`, s) 54 | } 55 | 56 | return state 57 | } 58 | 59 | export function useStoreOptionally(s: S, deps?: Deps>): [StoreV | undefined, boolean] { 60 | try { 61 | const store = useStore(s, deps) 62 | return [store, true] 63 | } catch (e) { 64 | if (e instanceof NoStoreError) { 65 | return [undefined, false] 66 | } else { 67 | throw e 68 | } 69 | } 70 | } 71 | 72 | function compare(oldDeps: unknown[], newDeps: unknown[]) { 73 | if (oldDeps.length !== newDeps.length) { 74 | return true 75 | } 76 | for (const index in newDeps) { 77 | if (oldDeps[index] !== newDeps[index]) { 78 | return true 79 | } 80 | } 81 | return false 82 | } 83 | -------------------------------------------------------------------------------- /src/container.tsx: -------------------------------------------------------------------------------- 1 | type Subscriber = () => void 2 | 3 | export class Container { 4 | subscribers = new Set() 5 | constructor( 6 | public state?: V 7 | ) {} 8 | initialized = false 9 | notify() { 10 | for (const subscriber of this.subscribers) { 11 | subscriber() 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/error.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store' 2 | 3 | export class NoStoreError { 4 | constructor( 5 | public message: string, 6 | public store: S, 7 | ) {} 8 | } 9 | -------------------------------------------------------------------------------- /src/executor.tsx: -------------------------------------------------------------------------------- 1 | import {FC, memo, useEffect} from 'react' 2 | import {Container} from './container' 3 | import {Store, StoreP, StoreV} from './store' 4 | 5 | interface Props { 6 | useStore: Store 7 | onChange: (value: StoreV) => void 8 | container: Container> 9 | args?: StoreP 10 | memo?: boolean 11 | } 12 | 13 | export const Executor: FC = memo(function Executor(props) { 14 | const args = props.args ?? [] 15 | const result = props.useStore(...args) 16 | props.container.state = result 17 | props.container.initialized = true 18 | useEffect(() => { 19 | props.onChange(result) 20 | props.container.notify() 21 | }) 22 | return null 23 | }, (prevProps, nextProps) => { 24 | if (!nextProps.memo) { 25 | return false 26 | } else { 27 | return argsAreEqual(prevProps.args, nextProps.args) 28 | } 29 | }) 30 | 31 | function argsAreEqual(args1: unknown[] | undefined, args2: unknown[] | undefined) { 32 | if (args1 === args2) return true 33 | if (args1.length !== args2.length) return false 34 | return args1.every((element, index) => element === args2[index]) 35 | } 36 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export {useStore, useStoreOptionally, Consumer} from './consumer' 2 | export {Provider} from './provider' 3 | export {Store} from './store' 4 | -------------------------------------------------------------------------------- /src/provider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {forwardRef, MutableRefObject, PropsWithChildren, Ref, useImperativeHandle, useRef, useState} from 'react' 3 | import {Container} from './container' 4 | import {Executor} from './executor' 5 | import {getStoreContext, Store, StoreP, StoreV} from './store' 6 | 7 | interface Props { 8 | of: S 9 | args?: StoreP 10 | memo?: boolean 11 | storeRef?: Ref> 12 | } 13 | 14 | export const Provider = function(props: PropsWithChildren>) { 15 | const Context = getStoreContext(props.of) 16 | 17 | const containerRef = useRef>>() 18 | if (!containerRef.current) { 19 | containerRef.current = new Container>() 20 | } 21 | const container = containerRef.current 22 | 23 | const checkRef = useRef() 24 | function onChange(value: StoreV) { 25 | if (props.storeRef) { 26 | if (typeof props.storeRef === 'function') { 27 | props.storeRef(value) 28 | } else { 29 | (props.storeRef as MutableRefObject>).current = value 30 | } 31 | } 32 | checkRef.current.onInitialize() 33 | } 34 | 35 | return ( 36 | <> 37 | 38 | 39 | 40 | {props.children} 41 | 42 | 43 | 44 | ) 45 | } 46 | 47 | Provider.displayName = 'Provider' 48 | 49 | Provider.defaultProps = { 50 | args: [], 51 | } 52 | 53 | interface CheckRef { 54 | onInitialize: () => void 55 | } 56 | 57 | const Checker = forwardRef 59 | }>>((props, ref) => { 60 | const [initialized, setInitialized] = useState(props.container.initialized) 61 | 62 | useImperativeHandle(ref, () => ({ 63 | onInitialize: () => { 64 | setInitialized(true) 65 | } 66 | })) 67 | 68 | return initialized && ( 69 | <> 70 | {props.children} 71 | 72 | ) 73 | }) 74 | 75 | Checker.displayName = 'Checker' 76 | -------------------------------------------------------------------------------- /src/store.ts: -------------------------------------------------------------------------------- 1 | import {Context, default as React} from 'react' 2 | import {Container} from './container' 3 | 4 | export interface Store

{ 5 | (...args: P): V 6 | displayName?: string 7 | // defaultProps? 8 | 9 | Context?: Context> 10 | } 11 | 12 | export type StoreP = S extends Store ? P : never 13 | export type StoreV = S extends Store ? V : never 14 | 15 | export const defaultStoreValue = Symbol() as any 16 | export function getStoreContext(s: S) { 17 | if (!s.Context) { 18 | s.Context = React.createContext(new Container(defaultStoreValue)) 19 | } 20 | return s.Context 21 | } 22 | -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hello World 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |

15 | 81 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": false, 4 | "declaration": true, 5 | "noImplicitAny": true, 6 | "target": "es6", 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "esModuleInterop": true, 10 | "jsx": "react", 11 | "lib": ["es6"], 12 | "outDir": "lib" 13 | }, 14 | "include": [ 15 | "src/**/*" 16 | ], 17 | "exclude": [ 18 | "**/__tests__", 19 | "node_modules" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "semicolon": [true, "never"], 9 | "trailing-comma": [true, {"multiline": { 10 | "typeLiterals": "never" 11 | }, "singleline": "never"}], 12 | "quotemark": [true, "single"], 13 | "interface-name": false, 14 | "no-console": false, 15 | "only-arrow-functions": false, 16 | "no-shadowed-variable": [true, { 17 | "function": false 18 | }], 19 | "no-trailing-whitespace": [true, "ignore-blank-lines"], 20 | "member-access": false, 21 | "curly": [true, "ignore-same-line"] 22 | }, 23 | "rulesDirectory": [] 24 | } 25 | --------------------------------------------------------------------------------