├── .editorconfig ├── .eslintignore ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ ├── build.yml │ ├── publish.yml │ └── review.yml ├── .gitignore ├── .npmrc ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── assets └── logo.png ├── babel.config.js ├── example ├── .storybook │ ├── main.js │ └── preview.js ├── app.json ├── babel.config.js ├── index.js ├── metro.config.js ├── package.json ├── src │ ├── App.tsx │ ├── Components │ │ ├── HomePage.tsx │ │ └── ToggleButton.tsx │ ├── assets │ │ └── logo.png │ ├── color │ │ └── color.tsx │ ├── hooks │ │ └── useDarkMode.tsx │ └── stories │ │ ├── API.stories.mdx │ │ ├── Counter.tsx │ │ ├── CounterWithInitialValue.stories.tsx │ │ ├── CounterWithoutInitialValue.stories.tsx │ │ ├── GettingStarted.stories.mdx │ │ ├── Performance.stories.mdx │ │ └── ToggleExample.tsx ├── webpack.config.js └── yarn.lock ├── jest.config.js ├── package.json ├── src ├── __tests__ │ └── index.test.tsx └── index.tsx ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | indent_style = space 10 | indent_size = 2 11 | 12 | end_of_line = lf 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules 3 | storybook-static 4 | web-build 5 | coverage 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | # specific for windows script files 3 | *.bat text eol=crlf 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: [buymeacoffee.com/daniakash] 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | push: 4 | branches: 5 | - master 6 | tags: 7 | - '!*' 8 | paths: 9 | - example/* 10 | - src/* 11 | - test/* 12 | - __tests__/* 13 | - '*.json' 14 | - yarn.lock 15 | - .github/**/*.yml 16 | 17 | jobs: 18 | lint: 19 | name: lint 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@master 23 | - uses: actions/setup-node@master 24 | with: 25 | node-version: 12.x 26 | - run: npx yarn bootstrap 27 | - run: npx yarn typescript 28 | - run: npx yarn lint 29 | 30 | test: 31 | strategy: 32 | matrix: 33 | platform: [ubuntu-latest, macOS-latest] 34 | node: ['12.x'] 35 | name: test/node ${{ matrix.node }}/${{ matrix.platform }} 36 | runs-on: ${{ matrix.platform }} 37 | steps: 38 | - uses: actions/checkout@master 39 | - uses: actions/setup-node@master 40 | with: 41 | node-version: ${{ matrix.node }} 42 | - run: npx yarn bootstrap 43 | - run: npx yarn test 44 | 45 | coverage: 46 | needs: [test, lint] 47 | name: coverage 48 | runs-on: ubuntu-latest 49 | steps: 50 | - uses: actions/checkout@master 51 | - uses: actions/setup-node@master 52 | with: 53 | node-version: 12.x 54 | - run: npx yarn bootstrap 55 | - uses: paambaati/codeclimate-action@v2.5.3 56 | env: 57 | CC_TEST_REPORTER_ID: ${{secrets.CC_TEST_REPORTER_ID}} 58 | with: 59 | coverageCommand: npx yarn test --coverage 60 | debug: true 61 | 62 | publish: 63 | needs: [test, lint] 64 | name: Publish example app to Expo 🚀 65 | runs-on: ubuntu-latest 66 | steps: 67 | - uses: actions/checkout@v2 68 | - uses: actions/setup-node@v1 69 | with: 70 | node-version: 12.x 71 | - uses: expo/expo-github-action@v5 72 | with: 73 | expo-version: 3.x 74 | expo-username: ${{ secrets.EXPO_CLI_USERNAME }} 75 | expo-password: ${{ secrets.EXPO_CLI_PASSWORD }} 76 | - run: npx yarn bootstrap 77 | - working-directory: example 78 | run: expo publish 79 | 80 | chromatic: 81 | needs: [test, lint] 82 | name: Publish storybook to chromatic 🧪 83 | runs-on: ubuntu-latest 84 | steps: 85 | - uses: actions/checkout@v2 86 | - uses: actions/setup-node@v1 87 | with: 88 | node-version: 12.x 89 | - run: npx yarn bootstrap 90 | - run: npx yarn chromatic 91 | working-directory: example 92 | env: 93 | CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} 94 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | on: 3 | release: 4 | types: [published] 5 | 6 | jobs: 7 | lint: 8 | name: lint 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@master 12 | - uses: actions/setup-node@master 13 | with: 14 | node-version: 12.x 15 | - run: npx yarn bootstrap 16 | - run: npx yarn typescript 17 | - run: npx yarn lint 18 | 19 | test: 20 | strategy: 21 | matrix: 22 | platform: [ubuntu-latest, macOS-latest] 23 | node: ['12.x'] 24 | name: test/node ${{ matrix.node }}/${{ matrix.platform }} 25 | runs-on: ${{ matrix.platform }} 26 | steps: 27 | - uses: actions/checkout@master 28 | - uses: actions/setup-node@master 29 | with: 30 | node-version: ${{ matrix.node }} 31 | - run: npx yarn bootstrap 32 | - run: npx yarn test 33 | 34 | publish: 35 | needs: [test, lint] 36 | name: Publish to npm 🚢📦 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@master 40 | - uses: actions/setup-node@master 41 | with: 42 | node-version: 12.x 43 | - run: npx yarn bootstrap 44 | - uses: JS-DevTools/npm-publish@v1 45 | with: 46 | token: ${{ secrets.NPM_TOKEN }} 47 | -------------------------------------------------------------------------------- /.github/workflows/review.yml: -------------------------------------------------------------------------------- 1 | name: review 2 | on: pull_request 3 | 4 | jobs: 5 | lint: 6 | name: lint 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@master 10 | - uses: actions/setup-node@master 11 | with: 12 | node-version: 12.x 13 | - run: npx yarn bootstrap 14 | - run: npx yarn typescript 15 | - run: npx yarn lint 16 | 17 | test: 18 | strategy: 19 | matrix: 20 | platform: [ubuntu-latest, macOS-latest] 21 | node: ['12.x'] 22 | name: test/node ${{ matrix.node }}/${{ matrix.platform }} 23 | runs-on: ${{ matrix.platform }} 24 | steps: 25 | - uses: actions/checkout@master 26 | - uses: actions/setup-node@master 27 | with: 28 | node-version: ${{ matrix.node }} 29 | - run: npx yarn bootstrap 30 | - run: npx yarn test 31 | 32 | coverage: 33 | needs: [test, lint] 34 | name: coverage 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@master 38 | - uses: actions/setup-node@master 39 | with: 40 | node-version: 12.x 41 | - run: npx yarn bootstrap 42 | - run: npx yarn test --coverage 43 | - uses: romeovs/lcov-reporter-action@v0.2.16 44 | with: 45 | github-token: ${{ secrets.GITHUB_TOKEN }} 46 | 47 | chromatic: 48 | needs: [test, lint] 49 | name: Publish storybook to chromatic 🧪 50 | runs-on: ubuntu-latest 51 | steps: 52 | - uses: actions/checkout@v2 53 | - uses: actions/setup-node@v1 54 | with: 55 | node-version: 12.x 56 | - run: npx yarn bootstrap 57 | - run: npx yarn chromatic 58 | working-directory: example 59 | env: 60 | CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} 61 | 62 | expo-publish: 63 | needs: [test, lint] 64 | name: Publish to Expo 🚀 65 | runs-on: ubuntu-latest 66 | steps: 67 | - uses: actions/checkout@v2 68 | - uses: actions/setup-node@v1 69 | with: 70 | node-version: 12.x 71 | - uses: expo/expo-github-action@v5 72 | with: 73 | expo-version: 3.x 74 | expo-username: ${{ secrets.EXPO_CLI_USERNAME }} 75 | expo-password: ${{ secrets.EXPO_CLI_PASSWORD }} 76 | - run: npx yarn bootstrap 77 | - run: expo publish --release-channel=pr-${{ github.event.number }} 78 | working-directory: example 79 | - uses: unsplash/comment-on-pr@master 80 | env: 81 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 82 | with: 83 | msg: App is ready for review, you can [see it here](https://expo.io/@daniakash/rex-state-example?release-channel=pr-${{ github.event.number }}). 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # logs 2 | *.log 3 | 4 | # coverage 5 | coverage 6 | 7 | # OSX 8 | # 9 | .DS_Store 10 | 11 | # storybook 12 | storybook-static 13 | 14 | # XDE 15 | .expo/ 16 | web-build 17 | 18 | # VSCode 19 | .vscode/ 20 | jsconfig.json 21 | 22 | # Xcode 23 | # 24 | build/ 25 | *.pbxuser 26 | !default.pbxuser 27 | *.mode1v3 28 | !default.mode1v3 29 | *.mode2v3 30 | !default.mode2v3 31 | *.perspectivev3 32 | !default.perspectivev3 33 | xcuserdata 34 | *.xccheckout 35 | *.moved-aside 36 | DerivedData 37 | *.hmap 38 | *.ipa 39 | *.xcuserstate 40 | project.xcworkspace 41 | 42 | # Android/IJ 43 | # 44 | .idea 45 | .gradle 46 | local.properties 47 | android.iml 48 | 49 | # Cocoapods 50 | # 51 | example/ios/Pods 52 | 53 | # node.js 54 | # 55 | node_modules/ 56 | npm-debug.log 57 | yarn-debug.log 58 | yarn-error.log 59 | 60 | # BUCK 61 | buck-out/ 62 | \.buckd/ 63 | android/app/libs 64 | android/keystores/debug.keystore 65 | 66 | # Expo 67 | .expo/* 68 | 69 | # generated by bob 70 | lib/ 71 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We want this community to be friendly and respectful to each other. Please follow it in all your interactions with the project. 4 | 5 | ## Development workflow 6 | 7 | To get started with the project, run `yarn bootstrap` in the root directory to install the required dependencies for each package: 8 | 9 | ```sh 10 | yarn bootstrap 11 | ``` 12 | 13 | While developing, you can run the [example app](/example/) to test your changes. 14 | 15 | To start the packager: 16 | 17 | ```sh 18 | yarn example start 19 | ``` 20 | 21 | To run the example app on Android: 22 | 23 | ```sh 24 | yarn example android 25 | ``` 26 | 27 | To run the example app on iOS: 28 | 29 | ```sh 30 | yarn example ios 31 | ``` 32 | 33 | Make sure your code passes TypeScript and ESLint. Run the following to verify: 34 | 35 | ```sh 36 | yarn typescript 37 | yarn lint 38 | ``` 39 | 40 | To fix formatting errors, run the following: 41 | 42 | ```sh 43 | yarn lint --fix 44 | ``` 45 | 46 | Remember to add tests for your change if possible. Run the unit tests by: 47 | 48 | ```sh 49 | yarn test 50 | ``` 51 | 52 | To edit the Objective-C files, open `example/ios/RexStateExample.xcworkspace` in XCode and find the source files at `Pods > Development Pods > rex-state`. 53 | 54 | To edit the Kotlin files, open `example/android` in Android studio and find the source files at `rexstate` under `Android`. 55 | 56 | ### Commit message convention 57 | 58 | We follow the [conventional commits specification](https://www.conventionalcommits.org/en) for our commit messages: 59 | 60 | - `fix`: bug fixes, e.g. fix crash due to deprecated method. 61 | - `feat`: new features, e.g. add new method to the module. 62 | - `refactor`: code refactor, e.g. migrate from class components to hooks. 63 | - `docs`: changes into documentation, e.g. add usage example for the module.. 64 | - `test`: adding or updating tests, eg add integration tests using detox. 65 | - `chore`: tooling changes, e.g. change CI config. 66 | 67 | Our pre-commit hooks verify that your commit message matches this format when committing. 68 | 69 | ### Linting and tests 70 | 71 | [ESLint](https://eslint.org/), [Prettier](https://prettier.io/), [TypeScript](https://www.typescriptlang.org/) 72 | 73 | We use [TypeScript](https://www.typescriptlang.org/) for type checking, [ESLint](https://eslint.org/) with [Prettier](https://prettier.io/) for linting and formatting the code, and [Jest](https://jestjs.io/) for testing. 74 | 75 | Our pre-commit hooks verify that the linter and tests pass when committing. 76 | 77 | ### Scripts 78 | 79 | The `package.json` file contains various scripts for common tasks: 80 | 81 | - `yarn bootstrap`: setup project by installing all dependencies and pods. 82 | - `yarn typescript`: type-check files with TypeScript. 83 | - `yarn lint`: lint files with ESLint. 84 | - `yarn test`: run unit tests with Jest. 85 | - `yarn example start`: start the Metro server for the example app. 86 | - `yarn example android`: run the example app on Android. 87 | - `yarn example ios`: run the example app on iOS. 88 | 89 | ### Sending a pull request 90 | 91 | > **Working on your first pull request?** You can learn how from this _free_ series: [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github). 92 | 93 | When you're sending a pull request: 94 | 95 | - Prefer small pull requests focused on one change. 96 | - Verify that linters and tests are passing. 97 | - Review the documentation to make sure it looks good. 98 | - Follow the pull request template when opening a pull request. 99 | - For pull requests that change the API or implementation, discuss with maintainers first by opening an issue. 100 | 101 | ## Code of Conduct 102 | 103 | ### Our Pledge 104 | 105 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 106 | 107 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 108 | 109 | ### Our Standards 110 | 111 | Examples of behavior that contributes to a positive environment for our community include: 112 | 113 | - Demonstrating empathy and kindness toward other people 114 | - Being respectful of differing opinions, viewpoints, and experiences 115 | - Giving and gracefully accepting constructive feedback 116 | - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 117 | - Focusing on what is best not just for us as individuals, but for the overall community 118 | 119 | Examples of unacceptable behavior include: 120 | 121 | - The use of sexualized language or imagery, and sexual attention or 122 | advances of any kind 123 | - Trolling, insulting or derogatory comments, and personal or political attacks 124 | - Public or private harassment 125 | - Publishing others' private information, such as a physical or email 126 | address, without their explicit permission 127 | - Other conduct which could reasonably be considered inappropriate in a 128 | professional setting 129 | 130 | ### Enforcement Responsibilities 131 | 132 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 133 | 134 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 135 | 136 | ### Scope 137 | 138 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 139 | 140 | ### Enforcement 141 | 142 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [INSERT CONTACT METHOD]. All complaints will be reviewed and investigated promptly and fairly. 143 | 144 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 145 | 146 | ### Enforcement Guidelines 147 | 148 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 149 | 150 | #### 1. Correction 151 | 152 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 153 | 154 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 155 | 156 | #### 2. Warning 157 | 158 | **Community Impact**: A violation through a single incident or series of actions. 159 | 160 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 161 | 162 | #### 3. Temporary Ban 163 | 164 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 165 | 166 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 167 | 168 | #### 4. Permanent Ban 169 | 170 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 171 | 172 | **Consequence**: A permanent ban from any sort of public interaction within the community. 173 | 174 | ### Attribution 175 | 176 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, 177 | available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 178 | 179 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 180 | 181 | [homepage]: https://www.contributor-covenant.org 182 | 183 | For answers to common questions about this code of conduct, see the FAQ at 184 | https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. 185 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-present DaniAkash 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 |
2 | 3 | rex-state-logo 9 | 10 | # Rex State 11 | 12 | Convert hooks into shared states between React components 13 | 14 | [![Build Status][build-badge]][build] 15 | [![Maintainability][maintainability-badge]][maintainability-url] 16 | [![Test Coverage][coverage-badge]][coverage-url] 17 | 18 | [![Version][version-badge]][package] 19 | [![Downloads][downloads-badge]][npmtrends] 20 | [![Bundlephobia][bundle-phobia-badge]][bundle-phobia] 21 | 22 | [![Star on GitHub][github-star-badge]][github-star] 23 | [![Watch on GitHub][github-watch-badge]][github-watch] 24 | [![Twitter Follow][twitter-badge]][twitter] 25 | 26 | [![donate][coffee-badge]][coffee-url] 27 | [![sponsor][sponsor-badge]][sponsor-url] 28 | [![support][support-badge]][support-url] 29 | 30 | [![Storybook][storybook-badge]][website] [![Chromatic][chromatic-badge]][chromatic] 31 | 32 | --- 33 | 34 | ### PRs Welcome 👍✨ 35 | 36 |
37 | 38 | - 📦 [Installation](#installation) 39 | - ℹ️ [Usage](#usage) 40 | - 📑 [Documentation][storybook-url] 41 | - 👨🏽‍🏫 Examples 42 | - [Simple Counter][codesandbox-example] with React.js on CodeSandbox 43 | - [Dark Mode][expo-app] with React Native on expo. Project in [`example/`](https://github.com/react-native-toolkit/rex-state/tree/master/example) directory 44 | - ✨ [Why Rex State?](#why-rex-state) 45 | 46 | ## Requirements 47 | 48 | Rex State is built purely on React Hooks hence it requires React > 16.8 to work. 49 | 50 | ## Installation 51 | 52 | ```sh 53 | yarn add rex-state 54 | 55 | # or 56 | 57 | npm i rex-state 58 | ``` 59 | 60 | ## Usage 61 | 62 | Consider the following hook which lets you toggle theme between light & dark modes 63 | 64 | ```jsx 65 | const useThemeMode = (initialTheme = 'light') => { 66 | const [theme, setTheme] = useState(initialTheme); 67 | 68 | const toggleTheme = () => setTheme(theme === 'light' ? 'dark' : 'light'); 69 | 70 | return [theme, toggleTheme]; 71 | }; 72 | ``` 73 | 74 | You can use the `createRexStore` module from rex state to create a provider & a store hook to access the result of your `useThemeMode` 75 | 76 | ```jsx 77 | import { createRexStore } from 'rex-state'; 78 | 79 | const { useStore: useTheme, RexProvider: ThemeProvider } = createRexStore( 80 | useThemeMode 81 | ); 82 | ``` 83 | 84 | The `useStore` hook & `RexProvider` are renamed to `useTheme` & `ThemeProvider` for use in the application. 85 | 86 | Now you can wrap your entire Application inside the `ThemeProvider` to ensure the context is setup properly for the `useTheme` hook. 87 | 88 | ```jsx 89 | const App = () => { 90 | return ( 91 | 92 | {/* Rest of your application */} 93 | 94 | 95 | 96 | ); 97 | }; 98 | ``` 99 | 100 | > Note: The value of the argument of `useThemeMode` function - `initialTheme` is supplied to the `ThemeProvider` using the `value` prop. The `value` prop only supports a single argument. Hence if your hook requires multiple arguments, you can pass them as a single object 101 | 102 | Once you add the `ThemeProvider` to the top of your application's tree, the child components can now use the `useTheme` hook to access the result of your `useThemeMode` hook. This time, when you call `toggleTheme` in any of the child components, it will cause your entire application tree to re-render & all the components that use the `useTheme` hook will have the updated value! 103 | 104 | The following is a toggle button that toggles the theme when users click on it. 105 | 106 | ```jsx 107 | const ToggleButton = () => { 108 | const [theme, toggleTheme] = useTheme(); 109 | 110 | return ( 111 | 112 | Is Dark Mode? 113 | 114 | 115 | ); 116 | }; 117 | ``` 118 | 119 | The next component is a text block that simply displays the theme's mode 120 | 121 | ```jsx 122 | const ThemeText = () => { 123 | const [theme] = useTheme(); 124 | 125 | return ( 126 | <> 127 | Theme Mode: 128 | {theme} 129 | 130 | ); 131 | }; 132 | ``` 133 | 134 | Invoking the `toggleTheme` function from the `` component updates the `` component. This means your hook is now a shared state between the two components! 135 | 136 | Also, check out the [counter example](https://codesandbox.io/s/rex-counter-2m4zy?file=/src/App.js) from codesandbox 137 | 138 | Rex State is good for some use cases and not suitable for some use cases since it uses the [React Context](https://reactjs.org/docs/context.html#api) API which is considered inefficient as a change causes the entire React child tree to re-render. Read the [performance](https://rex-state.netlify.app/?path=/story/intro-performance--page) section to see how to use Rex State effectively. 139 | 140 | ## Why Rex State? 141 | 142 | Rex State is a handy utility to make your hooks more powerful. It is simple, un-opinionated & is very tiny! 143 | 144 | ## Licenses 145 | 146 | MIT © [DaniAkash][twitter] 147 | 148 | [codesandbox-example]: https://codesandbox.io/s/rex-counter-2m4zy?file=/src/App.js 149 | [storybook-url]: https://rex-state.netlify.app 150 | [expo-app]: https://expo.io/@daniakash/rex-state-example 151 | [coffee-badge]: https://img.shields.io/badge/-%E2%98%95%EF%B8%8F%20buy%20me%20a%20coffee-e85b46 152 | [coffee-url]: https://www.buymeacoffee.com/daniakash 153 | [sponsor-badge]: https://img.shields.io/badge/-%F0%9F%8F%85%20sponsor%20this%20project-e85b46 154 | [sponsor-url]: https://www.buymeacoffee.com/daniakash/e/6983 155 | [support-badge]: https://img.shields.io/badge/-Get%20Support-e85b46 156 | [support-url]: https://www.buymeacoffee.com/daniakash/e/7030 157 | [build]: https://github.com/react-native-toolkit/rex-state/actions 158 | [build-badge]: https://github.com/react-native-toolkit/rex-state/workflows/build/badge.svg 159 | [coverage-badge]: https://api.codeclimate.com/v1/badges/9bd775907eca8a3dbab3/test_coverage 160 | [coverage-url]: https://codeclimate.com/github/react-native-toolkit/rex-state/test_coverage 161 | [maintainability-badge]: https://api.codeclimate.com/v1/badges/9bd775907eca8a3dbab3/maintainability 162 | [maintainability-url]: https://codeclimate.com/github/react-native-toolkit/rex-state/maintainability 163 | [bundle-phobia-badge]: https://badgen.net/bundlephobia/minzip/rex-state 164 | [bundle-phobia]: https://bundlephobia.com/result?p=rex-state 165 | [downloads-badge]: https://img.shields.io/npm/dm/rex-state.svg 166 | [npmtrends]: http://www.npmtrends.com/rex-state 167 | [package]: https://www.npmjs.com/package/rex-state 168 | [version-badge]: https://img.shields.io/npm/v/rex-state.svg 169 | [twitter]: https://twitter.com/dani_akash_ 170 | [twitter-badge]: https://img.shields.io/twitter/follow/dani_akash_?style=social 171 | [github-watch-badge]: https://img.shields.io/github/watchers/DaniAkash/rex.svg?style=social 172 | [github-watch]: https://github.com/DaniAkash/rex/watchers 173 | [github-star-badge]: https://img.shields.io/github/stars/DaniAkash/rex.svg?style=social 174 | [github-star]: https://github.com/DaniAkash/rex/stargazers 175 | [storybook-badge]: https://cdn.jsdelivr.net/gh/storybookjs/brand@master/badge/badge-storybook.svg 176 | [website]: https://rex-state.netlify.app 177 | [chromatic-badge]: https://img.shields.io/badge/-chromatic-%23fc521f 178 | [chromatic]: https://chromatic.com/library?appId=5f5b21fe6f304800225bd9cf&branch=master 179 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-toolkit/rex-state/84324150eb2c2f5eb892488e42230619f30ef8d1/assets/logo.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /example/.storybook/main.js: -------------------------------------------------------------------------------- 1 | const { withUnimodules } = require('@expo/webpack-config/addons'); 2 | const { resolve } = require('path'); 3 | 4 | module.exports = { 5 | webpackFinal: async (config, { configType }) => { 6 | return withUnimodules(config, { projectRoot: resolve(__dirname, '../') }); 7 | }, 8 | stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], 9 | addons: ['@storybook/addon-links', '@storybook/addon-essentials'], 10 | }; 11 | -------------------------------------------------------------------------------- /example/.storybook/preview.js: -------------------------------------------------------------------------------- 1 | export const parameters = { 2 | actions: { argTypesRegex: '^on[A-Z].*' }, 3 | options: { 4 | storySort: { 5 | order: ['Intro', ['Getting Started', 'API', 'Performance']], 6 | }, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /example/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rex-state-example", 3 | "displayName": "RexState Example", 4 | "expo": { 5 | "userInterfaceStyle": "automatic", 6 | "name": "rex-state-example", 7 | "slug": "rex-state-example", 8 | "description": "Example app for rex-state", 9 | "privacy": "public", 10 | "version": "1.0.0", 11 | "platforms": ["ios", "android", "web"], 12 | "splash": { 13 | "image": "./src/assets/logo.png", 14 | "resizeMode": "contain", 15 | "backgroundColor": "#ffffff" 16 | }, 17 | "icon": "./src/assets/logo.png", 18 | "ios": { 19 | "supportsTablet": true 20 | }, 21 | "assetBundlePatterns": ["**/*"] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const pak = require('../package.json'); 3 | 4 | module.exports = function (api) { 5 | api.cache(true); 6 | 7 | return { 8 | presets: ['babel-preset-expo'], 9 | plugins: [ 10 | [ 11 | 'module-resolver', 12 | { 13 | alias: { 14 | // For development, we want to alias the library to the source 15 | [pak.name]: path.join(__dirname, '..', pak.source), 16 | }, 17 | }, 18 | ], 19 | ], 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import { registerRootComponent } from 'expo'; 2 | 3 | import App from './src/App'; 4 | 5 | // registerRootComponent calls AppRegistry.registerComponent('main', () => App); 6 | // It also ensures that whether you load the app in the Expo client or in a native build, 7 | // the environment is set up appropriately 8 | registerRootComponent(App); 9 | -------------------------------------------------------------------------------- /example/metro.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const blacklist = require('metro-config/src/defaults/blacklist'); 3 | const escape = require('escape-string-regexp'); 4 | const pak = require('../package.json'); 5 | 6 | const root = path.resolve(__dirname, '..'); 7 | 8 | const modules = Object.keys({ 9 | ...pak.peerDependencies, 10 | }); 11 | 12 | module.exports = { 13 | projectRoot: __dirname, 14 | watchFolders: [root], 15 | 16 | // We need to make sure that only one version is loaded for peerDependencies 17 | // So we blacklist them at the root, and alias them to the versions in example's node_modules 18 | resolver: { 19 | blacklistRE: blacklist( 20 | modules.map( 21 | (m) => 22 | new RegExp(`^${escape(path.join(root, 'node_modules', m))}\\/.*$`) 23 | ) 24 | ), 25 | 26 | extraNodeModules: modules.reduce((acc, name) => { 27 | acc[name] = path.join(__dirname, 'node_modules', name); 28 | return acc; 29 | }, {}), 30 | }, 31 | 32 | transformer: { 33 | getTransformOptions: async () => ({ 34 | transform: { 35 | experimentalImportSupport: false, 36 | inlineRequires: true, 37 | }, 38 | }), 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rex-state-example", 3 | "description": "Example app for rex-state", 4 | "version": "0.0.1", 5 | "private": true, 6 | "main": "index", 7 | "scripts": { 8 | "android": "expo start --android", 9 | "ios": "expo start --ios", 10 | "web": "expo start --web", 11 | "start": "expo start", 12 | "test": "jest", 13 | "storybook": "start-storybook -p 6006", 14 | "build-storybook": "build-storybook -s public --no-dll", 15 | "chromatic": "npx chromatic" 16 | }, 17 | "dependencies": { 18 | "expo": "^38.0.0", 19 | "expo-splash-screen": "^0.3.1", 20 | "expo-status-bar": "^1.0.0", 21 | "react": "16.11.0", 22 | "react-dom": "16.11.0", 23 | "react-native": "0.62.2", 24 | "react-native-appearance": "~0.3.3", 25 | "react-native-unimodules": "~0.10.1", 26 | "react-native-web": "^0.12.3" 27 | }, 28 | "devDependencies": { 29 | "@babel/core": "^7.9.6", 30 | "@babel/runtime": "^7.9.6", 31 | "@storybook/addon-actions": "6.0.20", 32 | "@storybook/addon-essentials": "6.0.20", 33 | "@storybook/addon-links": "6.0.20", 34 | "@storybook/react": "6.0.20", 35 | "babel-loader": "^8.1.0", 36 | "babel-plugin-module-resolver": "^4.0.0", 37 | "babel-preset-expo": "^8.2.3", 38 | "expo-cli": "^3.21.12", 39 | "react-is": "^16.13.1", 40 | "typescript": "4.0.2" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /example/src/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import HomePage from './Components/HomePage'; 3 | import ToggleButton from './Components/ToggleButton'; 4 | import { DarkModeProvider } from './hooks/useDarkMode'; 5 | 6 | export default function App() { 7 | return ( 8 | 9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /example/src/Components/HomePage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, Text, StyleSheet } from 'react-native'; 3 | import color from '../color/color'; 4 | import { useDarkMode } from '../hooks/useDarkMode'; 5 | import { StatusBar } from 'expo-status-bar'; 6 | 7 | const HomePage = () => { 8 | const [mode] = useDarkMode(); 9 | 10 | return ( 11 | 17 | 18 | 19 | Theme Mode: {mode} 20 | 21 | 22 | ); 23 | }; 24 | 25 | const styles = StyleSheet.create({ 26 | container: { 27 | flex: 1, 28 | alignItems: 'center', 29 | justifyContent: 'center', 30 | }, 31 | text: { 32 | fontSize: 24, 33 | }, 34 | mode: { 35 | fontWeight: 'bold', 36 | }, 37 | }); 38 | 39 | export default HomePage; 40 | -------------------------------------------------------------------------------- /example/src/Components/ToggleButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, StyleSheet, Text, Switch } from 'react-native'; 3 | import color from '../color/color'; 4 | import { useDarkMode } from '../hooks/useDarkMode'; 5 | 6 | const ToggleButton = () => { 7 | const [mode, toggleMode] = useDarkMode(); 8 | 9 | return ( 10 | 11 | 12 | Is Dark Mode? 13 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | const styles = StyleSheet.create({ 20 | toggleContainer: { 21 | position: 'absolute', 22 | top: 96, 23 | right: 24, 24 | flexDirection: 'row', 25 | alignItems: 'center', 26 | justifyContent: 'center', 27 | }, 28 | label: { 29 | fontSize: 24, 30 | marginRight: 16, 31 | }, 32 | }); 33 | 34 | export default ToggleButton; 35 | -------------------------------------------------------------------------------- /example/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-toolkit/rex-state/84324150eb2c2f5eb892488e42230619f30ef8d1/example/src/assets/logo.png -------------------------------------------------------------------------------- /example/src/color/color.tsx: -------------------------------------------------------------------------------- 1 | const color = { 2 | dark: { 3 | backgroundColor: 'black', 4 | textColor: 'white', 5 | }, 6 | light: { 7 | backgroundColor: 'white', 8 | textColor: 'black', 9 | }, 10 | }; 11 | 12 | export default color; 13 | -------------------------------------------------------------------------------- /example/src/hooks/useDarkMode.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { Appearance } from 'react-native-appearance'; 3 | import { createRexStore } from 'rex-state'; 4 | 5 | const colorScheme = Appearance.getColorScheme(); 6 | 7 | const currentMode = 8 | ['light', 'dark'].indexOf(colorScheme) > -1 9 | ? (colorScheme as colorSchemeTypes) 10 | : 'light'; 11 | 12 | type colorSchemeTypes = 'light' | 'dark'; 13 | 14 | type useDarkModeHookReturnType = [colorSchemeTypes, () => void]; 15 | 16 | /** 17 | * A simple dark mode hook which returns the current mode of the app 18 | */ 19 | const useDarkModeHook = ( 20 | defaultMode?: colorSchemeTypes 21 | ): useDarkModeHookReturnType => { 22 | const [mode, setMode] = useState( 23 | defaultMode || currentMode 24 | ); 25 | 26 | const toggleMode = () => setMode(mode === 'light' ? 'dark' : 'light'); 27 | 28 | return [mode, toggleMode]; 29 | }; 30 | 31 | /** 32 | * Using rex-state to convert the hook into a shared state 33 | */ 34 | export const { 35 | useStore: useDarkMode, 36 | RexProvider: DarkModeProvider, 37 | } = createRexStore(useDarkModeHook); 38 | -------------------------------------------------------------------------------- /example/src/stories/API.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/addon-docs/blocks'; 2 | 3 | 4 | 5 | ## `createRexStore` 6 | 7 | ```tsx 8 | createRexStore: (useRexState: (value?: V) => T) => ({ 9 | RexProvider: (props: { children: ReactNode; value?: V }) => JSX.Element; 10 | useStore: () => T; 11 | }); 12 | ``` 13 | 14 | `createRexStore` accepts your hook as the argument and returns an object with two properties ﹣ 15 | 16 | - `RexProvider` which is a "[`Provider`](https://reactjs.org/docs/context.html#contextprovider)" component that will let you pass your hook down the React component tree to all the components by storing it in React context. 17 | - `useStore` hook will fetch your hook from the React context into your current component. This is built on top of the react "[`useContext`](https://reactjs.org/docs/hooks-reference.html#usecontext)" hook. 18 | -------------------------------------------------------------------------------- /example/src/stories/Counter.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { createRexStore } from 'rex-state'; 3 | 4 | const useCounterHook = (initialCount?: number) => { 5 | const [count, setCount] = useState(initialCount || 0); 6 | 7 | const increaseCount = () => setCount(count + 1); 8 | const decreaseCount = () => setCount(count - 1); 9 | 10 | return { 11 | count, 12 | increaseCount, 13 | decreaseCount, 14 | }; 15 | }; 16 | 17 | const { useStore: useCounter, RexProvider: CountProvider } = createRexStore( 18 | useCounterHook 19 | ); 20 | 21 | const CountDisplay = () => { 22 | const { count } = useCounter(); 23 | 24 | return

{count}

; 25 | }; 26 | 27 | const Controls = () => { 28 | const { increaseCount, decreaseCount } = useCounter(); 29 | 30 | return ( 31 | <> 32 | 37 | 42 | 43 | ); 44 | }; 45 | 46 | export const CounterWithInitialValue = () => { 47 | return ( 48 | 49 | 50 | 51 | 52 | ); 53 | }; 54 | 55 | export const CounterWithoutInitialValue = () => { 56 | return ( 57 | 58 | 59 | 60 | 61 | ); 62 | }; 63 | -------------------------------------------------------------------------------- /example/src/stories/CounterWithInitialValue.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { Story, Meta } from '@storybook/react/types-6-0'; 3 | import { CounterWithInitialValue } from './Counter'; 4 | 5 | export default { 6 | title: 'Example/Counter with Initial Value', 7 | component: CounterWithInitialValue, 8 | } as Meta; 9 | 10 | const Template: Story<{}> = (args) => ; 11 | 12 | export const WithInitialValue = Template.bind({}); 13 | WithInitialValue.args = {}; 14 | -------------------------------------------------------------------------------- /example/src/stories/CounterWithoutInitialValue.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { Story, Meta } from '@storybook/react/types-6-0'; 3 | import { CounterWithoutInitialValue } from './Counter'; 4 | 5 | export default { 6 | title: 'Example/Counter without Initial Value', 7 | component: CounterWithoutInitialValue, 8 | } as Meta; 9 | 10 | const Template: Story<{}> = (args) => ; 11 | 12 | export const WithoutInitialValue = Template.bind({}); 13 | WithoutInitialValue.args = {}; 14 | -------------------------------------------------------------------------------- /example/src/stories/GettingStarted.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/addon-docs/blocks'; 2 | import { useDarkMode, DarkModeProvider } from '../hooks/useDarkMode'; 3 | import { ThemeText, ToggleButton } from './ToggleExample'; 4 | import { Text } from 'react-native'; 5 | 6 | 7 | 8 | rex-state-logo 14 |
15 |
16 | 17 | # Rex State 18 | 19 | Convert hooks into shared states between React components 20 | 21 | [![Build Status][build-badge]][build] 22 | [![Maintainability][maintainability-badge]][maintainability-url] 23 | [![Test Coverage][coverage-badge]][coverage-url] 24 | 25 | [![Version][version-badge]][package] 26 | [![Downloads][downloads-badge]][npmtrends] 27 | [![Bundlephobia][bundle-phobia-badge]][bundle-phobia] 28 | 29 | [![Star on GitHub][github-star-badge]][github-star] 30 | [![Watch on GitHub][github-watch-badge]][github-watch] 31 | [![Twitter Follow][twitter-badge]][twitter] 32 | 33 | [![donate][coffee-badge]][coffee-url] 34 | [![sponsor][sponsor-badge]][sponsor-url] 35 | [![support][support-badge]][support-url] 36 | 37 | Rex State allows you to convert the result of your hooks into a shared state between multiple React components using the Context API. 38 | 39 | ## Usage 40 | 41 | Consider the following hook which lets you toggle theme between light & dark modes 42 | 43 | ```jsx 44 | const useThemeMode = (initialTheme = 'light') => { 45 | const [theme, setTheme] = useState(initialTheme); 46 | 47 | const toggleTheme = () => setTheme(theme === 'light' ? 'dark' : 'light'); 48 | 49 | return [theme, toggleTheme]; 50 | }; 51 | ``` 52 | 53 | You can use the `createRexStore` module from rex state to create a provider & a store hook to access the result of your `useThemeMode` 54 | 55 | ```jsx 56 | import { createRexStore } from 'rex-state'; 57 | 58 | const { useStore: useTheme, RexProvider: ThemeProvider } = createRexStore( 59 | useThemeMode 60 | ); 61 | ``` 62 | 63 | The `useStore` hook & `RexProvider` are renamed to `useTheme` & `ThemeProvider` for use in the application. 64 | 65 | Now you can wrap your entire Application inside the `ThemeProvider` to ensure the context is setup properly for the `useTheme` hook. 66 | 67 | ```jsx 68 | const App = () => { 69 | return ( 70 | 71 | {/* Rest of your application */} 72 | 73 | 74 | 75 | ); 76 | }; 77 | ``` 78 | 79 | > Note: The value of the argument of `useThemeMode` function - `initialTheme` is supplied to the `ThemeProvider` using the `value` prop. The `value` prop only supports a single argument. Hence if your hook requires multiple arguments, you can pass them as a single object 80 | 81 | Once you add the `ThemeProvider` to the top of your application's tree, the child components can now use the `useTheme` hook to access the result of your `useThemeMode` hook. This time, when you call `toggleTheme` in any of the child components, it will cause your entire application tree to re-render & all the components that use the `useTheme` hook will have the updated value! 82 | 83 | The following is a toggle button that toggles the theme when users click on it. 84 | 85 | ```jsx 86 | const ToggleButton = () => { 87 | const [theme, toggleTheme] = useTheme(); 88 | 89 | return ( 90 | 91 | Is Dark Mode? 92 | 93 | 94 | ); 95 | }; 96 | ``` 97 | 98 | The next component is a text block that simply displays the theme's mode 99 | 100 | ```jsx 101 | const ThemeText = () => { 102 | const [theme] = useTheme(); 103 | 104 | return ( 105 | <> 106 | Theme Mode: 107 | {theme} 108 | 109 | ); 110 | }; 111 | ``` 112 | 113 | ## In Action 114 | 115 |
116 | 117 | 118 | 119 | 120 | 121 | 122 |
123 |
124 |
125 | 126 | As you can see, calling the `toggleTheme` function from the `` component updates the `` component. This means your hook is now a shared state between the two components! 127 | 128 | Also, check out the [counter example](https://codesandbox.io/s/rex-counter-2m4zy?file=/src/App.js) from codesandbox 129 | 130 | Rex State is good for some use cases and not suitable for some use cases since it uses the [React Context](https://reactjs.org/docs/context.html#api) API which is considered inefficient as a change causes the entire React child tree to re-render. Read the performance section to see how to use Rex State effectively. 131 | 132 | [coffee-badge]: https://img.shields.io/badge/-%E2%98%95%EF%B8%8F%20buy%20me%20a%20coffee-e85b46 133 | [coffee-url]: https://www.buymeacoffee.com/daniakash 134 | [sponsor-badge]: https://img.shields.io/badge/-%F0%9F%8F%85%20sponsor%20this%20project-e85b46 135 | [sponsor-url]: https://www.buymeacoffee.com/daniakash/e/6983 136 | [support-badge]: https://img.shields.io/badge/-Get%20Support-e85b46 137 | [support-url]: https://www.buymeacoffee.com/daniakash/e/7030 138 | [build]: https://github.com/react-native-toolkit/rex-state/actions 139 | [build-badge]: https://github.com/react-native-toolkit/rex-state/workflows/build/badge.svg 140 | [coverage-badge]: https://api.codeclimate.com/v1/badges/9bd775907eca8a3dbab3/test_coverage 141 | [coverage-url]: https://codeclimate.com/github/react-native-toolkit/rex-state/test_coverage 142 | [maintainability-badge]: https://api.codeclimate.com/v1/badges/9bd775907eca8a3dbab3/maintainability 143 | [maintainability-url]: https://codeclimate.com/github/react-native-toolkit/rex-state/maintainability 144 | [bundle-phobia-badge]: https://badgen.net/bundlephobia/minzip/rex-state 145 | [bundle-phobia]: https://bundlephobia.com/result?p=rex-state 146 | [downloads-badge]: https://img.shields.io/npm/dm/rex-state.svg 147 | [npmtrends]: http://www.npmtrends.com/rex-state 148 | [package]: https://www.npmjs.com/package/rex-state 149 | [version-badge]: https://img.shields.io/npm/v/rex-state.svg 150 | [twitter]: https://twitter.com/dani_akash_ 151 | [twitter-badge]: https://img.shields.io/twitter/follow/dani_akash_?style=social 152 | [github-watch-badge]: https://img.shields.io/github/watchers/DaniAkash/rex.svg?style=social 153 | [github-watch]: https://github.com/DaniAkash/rex/watchers 154 | [github-star-badge]: https://img.shields.io/github/stars/DaniAkash/rex.svg?style=social 155 | [github-star]: https://github.com/DaniAkash/rex/stargazers 156 | -------------------------------------------------------------------------------- /example/src/stories/Performance.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/addon-docs/blocks'; 2 | 3 | 4 | 5 | # Performance 6 | 7 | Rex State makes the values returned by Hooks shareable using the React Context API. But there are knows performance limitations since changing the value of Context causes the entire Application tree to re-render. 8 | 9 | ## When not to use Rex State 10 | 11 | Rex State is not a state management library. It is a utility to quickly share a hook between two components. Hence if you are working with data that changes often and is shared by a large number of components, it is a good idea to avoid rex state. 12 | 13 | Rex State used to have an [`InjectStore`](https://github.com/daniakash/rex-state/tree/9809c7a7a6f71c1644a7d94a058b5606cf49da11#performance) HOC which implemented the solutions suggested by [Dan Abramov](https://github.com/facebook/react/issues/15156#issuecomment-474590693). This HOC is removed in v1.0 as the goal of Rex State is no longer to be a state management tool. You can directly implement the performance optimizations suggested in [the comment](https://github.com/facebook/react/issues/15156#issuecomment-474590693) if you need. 14 | 15 | If you are looking for a powerful state management tool that can efficiently avoid unnecessary re-renders, you can try [recoil](https://recoiljs.org/) or [zustand](https://github.com/react-spring/zustand). 16 | 17 | ## When you can use Rex State 18 | 19 | Rex State is good for data that doesn't change often. For example, managing color schemes of dark mode & light mode throughout your application is a good use case. As the theme modes do not change often. Also, changing the mode usually requires all the components to re-render. 20 | 21 | If you have data that changes often but you want to share the data of your hook with a limited number of components alone, then instead of wrapping your entire application inside the `RexProvider`, you can only wrap the nearest common parent of the required components inside the `RexProvider`. This ensures the re-renders are limited only to that parent and the rest of your application is not disturbed. 22 | -------------------------------------------------------------------------------- /example/src/stories/ToggleExample.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, Text, View, Switch } from 'react-native'; 3 | import { useDarkMode } from '../hooks/useDarkMode'; 4 | 5 | const styles = StyleSheet.create({ 6 | textStyle: { 7 | padding: 8, 8 | borderWidth: 1, 9 | borderColor: 'black', 10 | }, 11 | toggleContainer: { 12 | flexDirection: 'row', 13 | alignItems: 'center', 14 | justifyContent: 'center', 15 | }, 16 | label: { 17 | fontSize: 24, 18 | marginRight: 16, 19 | }, 20 | }); 21 | 22 | export const ThemeText = () => { 23 | const [mode] = useDarkMode(); 24 | 25 | return ( 26 | <> 27 | Theme Mode: 28 | {mode} 29 | 30 | ); 31 | }; 32 | 33 | export const ToggleButton = () => { 34 | const [mode, toggleMode] = useDarkMode(); 35 | 36 | return ( 37 | 38 | Is Dark Mode? 39 | 40 | 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const createExpoWebpackConfigAsync = require('@expo/webpack-config'); 3 | const { resolver } = require('./metro.config'); 4 | 5 | const root = path.resolve(__dirname, '..'); 6 | const node_modules = path.join(__dirname, 'node_modules'); 7 | 8 | module.exports = async function (env, argv) { 9 | const config = await createExpoWebpackConfigAsync(env, argv); 10 | 11 | config.module.rules.push({ 12 | test: /\.(js|ts|tsx)$/, 13 | include: path.resolve(root, 'src'), 14 | use: 'babel-loader', 15 | }); 16 | 17 | // We need to make sure that only one version is loaded for peerDependencies 18 | // So we alias them to the versions in example's node_modules 19 | Object.assign(config.resolve.alias, { 20 | ...resolver.extraNodeModules, 21 | 'react-native-web': path.join(node_modules, 'react-native-web'), 22 | }); 23 | 24 | return config; 25 | }; 26 | -------------------------------------------------------------------------------- /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 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | // bail: 0, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | // browser: false, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/private/var/folders/rl/3dd6tc2j3qv82p8kxpgjf0vr0000gn/T/jest_dx", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | clearMocks: true, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | // collectCoverage: false, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | // collectCoverageFrom: undefined, 25 | 26 | // The directory where Jest should output its coverage files 27 | coverageDirectory: 'coverage', 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | // coveragePathIgnorePatterns: [ 31 | // "/node_modules/" 32 | // ], 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | // coverageReporters: [ 36 | // "json", 37 | // "text", 38 | // "lcov", 39 | // "clover" 40 | // ], 41 | 42 | // An object that configures minimum threshold enforcement for coverage results 43 | // coverageThreshold: undefined, 44 | 45 | // A path to a custom dependency extractor 46 | // dependencyExtractor: undefined, 47 | 48 | // Make calling deprecated APIs throw helpful error messages 49 | // errorOnDeprecated: false, 50 | 51 | // Force coverage collection from ignored files using an array of glob patterns 52 | // forceCoverageMatch: [], 53 | 54 | // A path to a module which exports an async function that is triggered once before all test suites 55 | // globalSetup: undefined, 56 | 57 | // A path to a module which exports an async function that is triggered once after all test suites 58 | // globalTeardown: undefined, 59 | 60 | // A set of global variables that need to be available in all test environments 61 | // globals: {}, 62 | 63 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 64 | // maxWorkers: "50%", 65 | 66 | // An array of directory names to be searched recursively up from the requiring module's location 67 | // moduleDirectories: [ 68 | // "node_modules" 69 | // ], 70 | 71 | // An array of file extensions your modules use 72 | // moduleFileExtensions: [ 73 | // "js", 74 | // "json", 75 | // "jsx", 76 | // "ts", 77 | // "tsx", 78 | // "node" 79 | // ], 80 | 81 | // A map from regular expressions to module names that allow to stub out resources with a single module 82 | // moduleNameMapper: {}, 83 | 84 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 85 | // modulePathIgnorePatterns: [], 86 | 87 | // Activates notifications for test results 88 | // notify: false, 89 | 90 | // An enum that specifies notification mode. Requires { notify: true } 91 | // notifyMode: "failure-change", 92 | 93 | // A preset that is used as a base for Jest's configuration 94 | // preset: undefined, 95 | 96 | // Run tests from one or more projects 97 | // projects: undefined, 98 | 99 | // Use this configuration option to add custom reporters to Jest 100 | // reporters: undefined, 101 | 102 | // Automatically reset mock state between every test 103 | // resetMocks: false, 104 | 105 | // Reset the module registry before running each individual test 106 | // resetModules: false, 107 | 108 | // A path to a custom resolver 109 | // resolver: undefined, 110 | 111 | // Automatically restore mock state between every test 112 | // restoreMocks: false, 113 | 114 | // The root directory that Jest should scan for tests and modules within 115 | // rootDir: undefined, 116 | 117 | // A list of paths to directories that Jest should use to search for files in 118 | // roots: [ 119 | // "" 120 | // ], 121 | 122 | // Allows you to use a custom runner instead of Jest's default test runner 123 | // runner: "jest-runner", 124 | 125 | // The paths to modules that run some code to configure or set up the testing environment before each test 126 | // setupFiles: [], 127 | 128 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 129 | // setupFilesAfterEnv: [], 130 | 131 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 132 | // snapshotSerializers: [], 133 | 134 | // The test environment that will be used for testing 135 | // testEnvironment: "jest-environment-jsdom", 136 | 137 | // Options that will be passed to the testEnvironment 138 | // testEnvironmentOptions: {}, 139 | 140 | // Adds a location field to test results 141 | // testLocationInResults: false, 142 | 143 | // The glob patterns Jest uses to detect test files 144 | // testMatch: [ 145 | // "**/__tests__/**/*.[jt]s?(x)", 146 | // "**/?(*.)+(spec|test).[tj]s?(x)" 147 | // ], 148 | 149 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 150 | testPathIgnorePatterns: ['/node_modules/', '/lib/'], 151 | 152 | // The regexp pattern or array of patterns that Jest uses to detect test files 153 | // testRegex: [], 154 | 155 | // This option allows the use of a custom results processor 156 | // testResultsProcessor: undefined, 157 | 158 | // This option allows use of a custom test runner 159 | // testRunner: "jasmine2", 160 | 161 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 162 | // testURL: "http://localhost", 163 | 164 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 165 | // timers: "real", 166 | 167 | // A map from regular expressions to paths to transformers 168 | // transform: undefined, 169 | 170 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 171 | // transformIgnorePatterns: [ 172 | // "/node_modules/" 173 | // ], 174 | 175 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 176 | // unmockedModulePathPatterns: undefined, 177 | 178 | // Indicates whether each individual test should be reported during the run 179 | // verbose: undefined, 180 | 181 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 182 | // watchPathIgnorePatterns: [], 183 | 184 | // Whether to use watchman for file crawling 185 | // watchman: true, 186 | }; 187 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rex-state", 3 | "version": "1.0.0", 4 | "description": "Convert hooks into shared states between React components", 5 | "main": "lib/commonjs/index", 6 | "module": "lib/module/index", 7 | "types": "lib/typescript/src/index.d.ts", 8 | "react-native": "src/index", 9 | "source": "src/index", 10 | "files": [ 11 | "src", 12 | "lib", 13 | "android", 14 | "ios", 15 | "cpp", 16 | "rex-state.podspec", 17 | "!lib/typescript/example", 18 | "!**/__tests__", 19 | "!**/__fixtures__", 20 | "!**/__mocks__" 21 | ], 22 | "scripts": { 23 | "test": "jest", 24 | "typescript": "tsc --noEmit", 25 | "lint": "eslint \"**/*.{js,ts,tsx}\"", 26 | "prepare": "yarn example && yarn pods && bob build", 27 | "release": "release-it", 28 | "example": "yarn --cwd example", 29 | "pods": "cd example && pod-install --quiet", 30 | "bootstrap": "yarn example && yarn && yarn pods" 31 | }, 32 | "keywords": [ 33 | "react", 34 | "state-management", 35 | "state", 36 | "rex", 37 | "redux", 38 | "rex-state", 39 | "userex-hook", 40 | "react-hooks" 41 | ], 42 | "repository": "https://github.com/react-native-toolkit/rex-state", 43 | "author": "DaniAkash (https://github.com/DaniAkash)", 44 | "license": "MIT", 45 | "bugs": { 46 | "url": "https://github.com/react-native-toolkit/rex-state/issues" 47 | }, 48 | "homepage": "https://rex-state.netlify.app", 49 | "devDependencies": { 50 | "@commitlint/config-conventional": "^8.3.4", 51 | "@react-native-community/bob": "^0.16.2", 52 | "@react-native-community/eslint-config": "^2.0.0", 53 | "@release-it/conventional-changelog": "^1.1.4", 54 | "@testing-library/react": "11.0.2", 55 | "@types/jest": "^26.0.0", 56 | "@types/react": "^16.9.19", 57 | "@types/react-native": "0.62.13", 58 | "commitlint": "^8.3.5", 59 | "eslint": "^7.2.0", 60 | "eslint-config-prettier": "^6.11.0", 61 | "eslint-plugin-prettier": "^3.1.3", 62 | "husky": "^4.2.5", 63 | "jest": "^26.0.1", 64 | "pod-install": "^0.1.0", 65 | "prettier": "^2.0.5", 66 | "react": "16.11.0", 67 | "react-dom": "16.13.1", 68 | "react-native": "0.62.2", 69 | "release-it": "^13.5.8", 70 | "typescript": "^3.8.3" 71 | }, 72 | "peerDependencies": { 73 | "react": "*", 74 | "react-native": "*" 75 | }, 76 | "jest": { 77 | "preset": "react-native", 78 | "modulePathIgnorePatterns": [ 79 | "/example/node_modules", 80 | "/lib/" 81 | ] 82 | }, 83 | "husky": { 84 | "hooks": { 85 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 86 | "pre-commit": "yarn lint && yarn typescript" 87 | } 88 | }, 89 | "commitlint": { 90 | "extends": [ 91 | "@commitlint/config-conventional" 92 | ] 93 | }, 94 | "release-it": { 95 | "git": { 96 | "commitMessage": "chore: release ${version}", 97 | "tagName": "v${version}" 98 | }, 99 | "npm": { 100 | "publish": true 101 | }, 102 | "github": { 103 | "release": true 104 | }, 105 | "plugins": { 106 | "@release-it/conventional-changelog": { 107 | "preset": "angular" 108 | } 109 | } 110 | }, 111 | "eslintConfig": { 112 | "extends": [ 113 | "@react-native-community", 114 | "prettier" 115 | ], 116 | "rules": { 117 | "prettier/prettier": [ 118 | "error", 119 | { 120 | "quoteProps": "consistent", 121 | "singleQuote": true, 122 | "tabWidth": 2, 123 | "trailingComma": "es5", 124 | "useTabs": false 125 | } 126 | ] 127 | } 128 | }, 129 | "eslintIgnore": [ 130 | "node_modules/", 131 | "lib/" 132 | ], 133 | "prettier": { 134 | "quoteProps": "consistent", 135 | "singleQuote": true, 136 | "tabWidth": 2, 137 | "trailingComma": "es5", 138 | "useTabs": false 139 | }, 140 | "@react-native-community/bob": { 141 | "source": "src", 142 | "output": "lib", 143 | "targets": [ 144 | "commonjs", 145 | "module", 146 | "typescript" 147 | ] 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { render, cleanup, act } from '@testing-library/react'; 3 | import { createRexStore } from '../index'; 4 | 5 | const useInput = (defaultValue: string = '') => { 6 | const [title] = useState('Sample Input'); 7 | const [value, setValue] = useState(defaultValue); 8 | 9 | const updateValue = (newValue: string) => { 10 | setValue(newValue); 11 | }; 12 | 13 | return { 14 | title, 15 | value, 16 | updateValue, 17 | }; 18 | }; 19 | 20 | type useInputReturnType = ReturnType; 21 | 22 | const InputField = ({ 23 | children, 24 | defaultValue, 25 | }: { 26 | children: (hook: useInputReturnType) => any; 27 | defaultValue?: string; 28 | }) => children(useInput(defaultValue)); 29 | 30 | const setupInputField = (props: { defaultValue?: string } = {}) => { 31 | const returnValue = {} as useInputReturnType; 32 | render( 33 | 34 | {(val) => { 35 | Object.assign(returnValue, val); 36 | return null; 37 | }} 38 | 39 | ); 40 | return returnValue; 41 | }; 42 | 43 | const { RexProvider, useStore } = createRexStore(useInput); 44 | 45 | const InputFieldWithStore = ({ 46 | children, 47 | }: { 48 | children: (hook: useInputReturnType) => any; 49 | }) => children(useStore()); 50 | 51 | const setupInputFieldWithStore = (props: { defaultValue?: string } = {}) => { 52 | const returnValue = {} as useInputReturnType; 53 | render( 54 | 55 | 56 | {(val) => { 57 | Object.assign(returnValue, val); 58 | return null; 59 | }} 60 | 61 | 62 | ); 63 | return returnValue; 64 | }; 65 | 66 | const setupInputFieldWithError = () => { 67 | const returnValue = {} as useInputReturnType; 68 | render( 69 | 70 | {(val) => { 71 | Object.assign(returnValue, val); 72 | return null; 73 | }} 74 | 75 | ); 76 | return returnValue; 77 | }; 78 | 79 | afterEach(cleanup); 80 | 81 | describe('Testing Rex State', () => { 82 | it('useInput - no default value - without rex state', () => { 83 | const inputData = setupInputField(); 84 | expect(inputData.title).toBe('Sample Input'); 85 | expect(inputData.value).toBe(''); 86 | act(() => { 87 | inputData.updateValue('New Text'); 88 | }); 89 | expect(inputData.value).toBe('New Text'); 90 | }); 91 | 92 | it('useInput - with default value - without rex state', () => { 93 | const inputData = setupInputField({ defaultValue: 'Default Text' }); 94 | expect(inputData.title).toBe('Sample Input'); 95 | expect(inputData.value).toBe('Default Text'); 96 | act(() => { 97 | inputData.updateValue('New Text'); 98 | }); 99 | expect(inputData.value).toBe('New Text'); 100 | }); 101 | 102 | it('useInput - no default value - with rex state', () => { 103 | const inputData = setupInputFieldWithStore(); 104 | expect(inputData.title).toBe('Sample Input'); 105 | expect(inputData.value).toBe(''); 106 | act(() => { 107 | inputData.updateValue('New Text'); 108 | }); 109 | expect(inputData.value).toBe('New Text'); 110 | }); 111 | 112 | it('useInput - with default value - with rex state', () => { 113 | const inputData = setupInputFieldWithStore({ 114 | defaultValue: 'Default Text', 115 | }); 116 | expect(inputData.title).toBe('Sample Input'); 117 | expect(inputData.value).toBe('Default Text'); 118 | act(() => { 119 | inputData.updateValue('New Text'); 120 | }); 121 | expect(inputData.value).toBe('New Text'); 122 | }); 123 | 124 | it('useStore without provider - should throw error', () => { 125 | const renderModule = () => { 126 | const result = setupInputFieldWithError(); 127 | return result; 128 | }; 129 | expect(renderModule).toThrow(); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, ReactNode } from 'react'; 2 | 3 | export const createRexStore = ( 4 | useRexState: (value?: V) => T 5 | ): { 6 | RexProvider: ({ 7 | children, 8 | value, 9 | }: { 10 | children: ReactNode; 11 | value?: V; 12 | }) => JSX.Element; 13 | useStore: () => T; 14 | } => { 15 | const RexContext = createContext T>>((null as any) as T); 16 | const { Provider } = RexContext; 17 | 18 | const useStore = () => { 19 | const store = useContext(RexContext); 20 | if (!store) { 21 | throw new Error( 22 | 'Component must be wrapped with a suitable ' 23 | ); 24 | } 25 | return store; 26 | }; 27 | 28 | const RexProvider = ({ 29 | children, 30 | value, 31 | }: { 32 | children: ReactNode; 33 | value?: V; 34 | }) => { 35 | const state = useRexState(value); 36 | return {children}; 37 | }; 38 | 39 | return { RexProvider, useStore }; 40 | }; 41 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "rex-state": ["./src/index"] 6 | }, 7 | "allowUnreachableCode": false, 8 | "allowUnusedLabels": false, 9 | "esModuleInterop": true, 10 | "importsNotUsedAsValues": "error", 11 | "forceConsistentCasingInFileNames": true, 12 | "jsx": "react", 13 | "lib": ["esnext"], 14 | "module": "esnext", 15 | "moduleResolution": "node", 16 | "noFallthroughCasesInSwitch": true, 17 | "noImplicitReturns": true, 18 | "noImplicitUseStrict": false, 19 | "noStrictGenericChecks": false, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "resolveJsonModule": true, 23 | "skipLibCheck": true, 24 | "strict": true, 25 | "target": "esnext" 26 | } 27 | } 28 | --------------------------------------------------------------------------------