├── .github └── workflows │ └── test.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── examples ├── preact-htm-browser │ ├── README.md │ ├── index.html │ └── script.js └── react-ts-vite │ ├── README.md │ ├── index.html │ ├── package.json │ ├── postcss.config.js │ ├── src │ ├── basic │ │ ├── createSetup.tsx │ │ ├── defineComponent.tsx │ │ └── useSetup.tsx │ ├── index.tsx │ ├── main.css │ ├── pinia │ │ ├── PiniaA.tsx │ │ ├── PiniaB.tsx │ │ └── store.ts │ ├── villus │ │ └── useQuery.tsx │ ├── vueuse-head │ │ └── head.tsx │ └── vueuse │ │ ├── useBattery.tsx │ │ └── useMouse.tsx │ ├── tailwind.config.js │ ├── tsconfig.json │ └── vite.config.ts ├── package.json ├── packages └── reactivue │ ├── LICENSE │ ├── babel.config.js │ ├── global.d.ts │ ├── jest.config.ts │ ├── package.json │ ├── rollup.config.js │ ├── setupTests.ts │ ├── src │ ├── component.ts │ ├── computed.ts │ ├── createSetup.ts │ ├── defineComponent.ts │ ├── errorHandling.ts │ ├── index.ts │ ├── lifecycle.ts │ ├── mock.ts │ ├── nextTick.ts │ ├── types.ts │ ├── useSetup.ts │ └── watch.ts │ ├── tests │ ├── computed.spec.tsx │ ├── createSetup.spec.tsx │ ├── defineComponent.spec.tsx │ ├── lifecycle.spec.tsx │ ├── mock.spec.tsx │ ├── useSetup.spec.tsx │ └── watch.spec.tsx │ └── tsconfig.json ├── screenshots └── logo.svg ├── tsconfig.json └── yarn.lock /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [12.x, 14.x, 15.x] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - run: yarn install --frozen-lockfile 25 | - run: yarn test 26 | - run: yarn build 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_Store 4 | .cache 5 | packages/reactivue/README.md 6 | .idea/ 7 | preact -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "Unmounting", 4 | "unmounts" 5 | ] 6 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-present, Anthony Fu 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

3 | 4 |

Use Vue Composition API in React components

5 | 6 |

7 | 8 | 9 |

10 | 11 | 12 |
 13 | npm i reactivue
 14 | 
15 | 16 |
17 | 18 |

I love Vue Composition API and its reactivity system,
but functional components in React are also sweet with Typescript.
Instead of making a choice, why not to use them together?

19 | 20 |
21 | 22 | 23 | ## Usage 24 | 25 | ### Component Factory 26 | 27 | ```tsx 28 | import React from 'React' 29 | import { defineComponent, ref, computed, onUnmounted } from 'reactivue' 30 | 31 | interface Props { 32 | value: number 33 | } 34 | 35 | const MyCounter = defineComponent( 36 | // setup function in Vue 37 | (props: Props) => { 38 | const counter = ref(props.value) 39 | const doubled = computed(() => counter.value * 2) 40 | const inc = () => counter.value += 1 41 | 42 | onUnmounted(() => console.log('Goodbye World')) 43 | 44 | return { counter, doubled, inc } 45 | }, 46 | // functional component in React 47 | ({ counter, doubled, inc }) => { 48 | // you can still use other React hooks 49 | return ( 50 |
51 |
{counter} x 2 = {doubled}
52 | 53 |
54 | ) 55 | } 56 | ) 57 | 58 | // use it as you normally would 59 | render(, el) 60 | ``` 61 | 62 | ### Hooks 63 | 64 | You can use it as a hook as well. 65 | 66 | > The `defineComponent` factory is actually a sugar to and equivalent to the following code. 67 | 68 | 69 | ```tsx 70 | import React from 'React' 71 | import { useSetup, ref, computed, onUnmounted } from 'reactivue' 72 | 73 | interface Props { 74 | value: number 75 | } 76 | 77 | function MyCounter(Props: Props) { 78 | const state = useSetup( 79 | (props: Props) => { // props is a reactive object in Vue 80 | const counter = ref(props.value) 81 | const doubled = computed(() => counter.value * 2) 82 | const inc = () => counter.value += 1 83 | 84 | onUnmounted(() => console.log('Goodbye World')) 85 | 86 | return { counter, doubled, inc } 87 | }, 88 | Props // pass React props to it 89 | ) 90 | 91 | // state is a plain object just like React state 92 | const { counter, doubled, inc } = state 93 | 94 | return ( 95 |
96 |
{counter} x 2 = {doubled}
97 | 98 |
99 | ) 100 | } 101 | ``` 102 | 103 | ### Hook Factory 104 | 105 | To reuse the composition logics, `createSetup` is provided as a factory to create your own hooks. 106 | 107 | ```ts 108 | // mySetup.ts 109 | import { createSetup, ref, computed, onUnmounted } from 'reactivue' 110 | 111 | export interface Props { 112 | value: number 113 | } 114 | 115 | // create a custom hook that can be reused 116 | export const useMySetup = createSetup( 117 | (props: Props) => { 118 | const counter = ref(props.value) 119 | const doubled = computed(() => counter.value * 2) 120 | const inc = () => counter.value += 1 121 | 122 | onUnmounted(() => console.log('Goodbye World')) 123 | 124 | return { counter, doubled, inc } 125 | }, 126 | ) 127 | ``` 128 | 129 | ```tsx 130 | // Counter.tsx 131 | import React from 'react' 132 | import { useMySetup, Props } from './mySetup' 133 | 134 | export const Counter = (props: Props) => { 135 | const { counter, doubled, inc } = useMySetup(props) 136 | const { counter: counter2, doubled: doubled2, inc: inc2 } = useMySetup({ value: 10 }) 137 | 138 | return ( 139 |
140 |
{counter} x 2 = {doubled}
141 | 142 |
143 | 144 |
{counter2} x 2 = {doubled2}
145 | 146 |
147 | ) 148 | } 149 | ``` 150 | 151 | ## Usage with Preact 152 | 153 | To use reactivue in Preact apps, just replace `reactivue` import with `reactivue/preact` 154 | 155 | ```diff 156 | import { h } from 'preact' 157 | -import { defineComponent, ref, computed, onUnmounted } from 'reactivue' 158 | +import { defineComponent, ref, computed, onUnmounted } from 'reactivue/preact' 159 | ``` 160 | 161 | ## Using Vue's Libraries 162 | 163 | *Yes, you can!* Before you start, you need set alias in your build tool in order to redirect some apis from `vue` to `reactivue` or `reactivue/preact` if you are using it with Preact. 164 | 165 | #### Aliasing 166 | 167 |
168 | Vite
169 | 170 | Add following code to `vite.config.js` 171 | 172 | ```js 173 | { 174 | /* ... */ 175 | alias: { 176 | 'vue': 'reactivue', 177 | '@vue/runtime-dom': 'reactivue', 178 | } 179 | } 180 | ``` 181 | 182 | If you are using it with Preact you have to add following code to `vite.config.js` 183 | 184 | ```ts 185 | { 186 | /* ... */ 187 | optimizeDeps: { 188 | include: ['reactivue/preact'], 189 | exclude: ['@vue/reactivity'] 190 | } 191 | } 192 | ``` 193 | 194 |
195 |
196 | Webpack
197 | 198 | Add following code to your webpack config 199 | 200 | ```js 201 | const config = { 202 | /* ... */ 203 | resolve: { 204 | alias: { 205 | 'vue': 'reactivue', 206 | '@vue/runtime-dom': 'reactivue', 207 | }, 208 | } 209 | } 210 | ``` 211 | 212 |
213 | 214 |
215 | Parcel
216 | 217 | Parcel uses the standard `package.json` file to read configuration options under an `alias` key. 218 | 219 | ```js 220 | { 221 | "alias": { 222 | "vue": "reactivue", 223 | "@vue/runtime-dom": "reactivue", 224 | }, 225 | } 226 | ``` 227 | 228 |
229 | 230 | 231 |
232 | Rollup
233 | 234 | To alias within Rollup, you'll need to install [@rollup/plugin-alias](https://github.com/rollup/plugins/tree/master/packages/alias). The plugin will need to be placed before your `@rollup/plugin-node-resolve`. 235 | 236 | ```js 237 | import alias from '@rollup/plugin-alias'; 238 | 239 | module.exports = { 240 | plugins: [ 241 | alias({ 242 | entries: [ 243 | { find: 'vue', replacement: 'reactivue' }, 244 | { find: '@vue/runtime-dom', replacement: 'reactivue' } 245 | ] 246 | }) 247 | ] 248 | }; 249 | ``` 250 | 251 |
252 | 253 | 254 |
255 | Jest
256 | 257 | Jest allows the rewriting of module paths similar to bundlers. These rewrites are configured using regular expressions in your Jest configuration: 258 | 259 | ```js 260 | { 261 | "moduleNameMapper": { 262 | "^vue$": "reactivue", 263 | "^@vue/runtime-dom$": "reactivue", 264 | } 265 | } 266 | ``` 267 | 268 |
269 | 270 | #### Installing Vue Plugins 271 | 272 | Installing Vue plugins are almost identical to Vue. Just simply create your root instance with `createApp` function and register your plugins as you do in Vue apps. **You don't need to call `app.mount`**. Your Vue plugins will be available in all your setup functions. 273 | 274 | ```ts 275 | import { createApp } from 'reactivue' 276 | import { createPinia } from 'pinia' 277 | 278 | const app = createApp() 279 | 280 | app.use(createPinia()) 281 | ``` 282 | 283 | > Note: If you are trying to use a library that calls app.component, app.directive or app.mixin in its install function, reactivue will skip these calls without any action and warn you about it. 284 | 285 | #### Compatible Libraries 286 | 287 | > A list of libaries that have been tested to work with `reactivue`. Feel free to make PRs adding more. 288 | 289 | - [pinia](https://github.com/posva/pinia) - 🍍 Automatically Typed, Modular and lightweight Store for Vue 290 | - [VueUse](https://github.com/vueuse/vueuse) - 🧰 Collection of Composition API utils for Vue 2 and 3 291 | - [Villus](https://github.com/logaretm/villus) - 🏎 A tiny and fast GraphQL client for Vue.js 292 | 293 | ## APIs 294 | 295 | Some tips and cavert compare to Vue's Composition API. 296 | 297 | #### Reactivity 298 | 299 | The reactivity system APIs are direct re-exported from `@vue/reactivity`, they should work the same as in Vue. 300 | 301 | ````ts 302 | // the following two line are equivalent. 303 | import { ref, reactive, computed } from 'reactivue' 304 | import { ref, reactive, computed } from '@vue/reactivity' 305 | ```` 306 | 307 | #### Lifecycles 308 | 309 | This library implemented the basic lifecycles to bound with React's lifecycles. For some lifecycles that don't have the React equivalent, they will be called somewhere near when they should be called (for example `onMounted` will be call right after `onCreated`). 310 | 311 | For most of the time, you can use them like you would in Vue. 312 | 313 | #### Extra APIs 314 | 315 | - `defineComponent()` - not the one you expected to see in Vue. Instead, it accepts a setup function and a render function that will return a React Functional Component. 316 | - `useSetup()` - the hook for resolve Composition API's setup, refer to the section above. 317 | - `createSetup()` - a factory to wrapper your logics into reusable custom hooks. 318 | 319 | 320 | #### Limitations 321 | 322 | - `getCurrentInstance()` - returns the meta info for the internal states, NOT a Vue instance. It's exposed to allow you check if it's inside a instance scope. 323 | - `emit()` is not available 324 | 325 | 326 | ### Examples 327 | 328 | #### Real-world Examples/Showcases 329 | 330 | - [Café CN](https://github.com/antfu/awesome-cn-cafe-web) - Web App for Awesome CN Café 331 | 332 | - [Preact Browser Example](./examples/preact-htm-browser) 333 | 334 | - [React Vite 2.0 Demo](./examples/react-ts-vite) 335 | 336 | ![image](https://user-images.githubusercontent.com/11247099/88056258-dd7f6980-cb92-11ea-9e89-e090e73b7235.png) 337 | 338 | ### License 339 | 340 | [MIT License](https://github.com/antfu/rectivue/blob/master/LICENSE) © 2020 [Anthony Fu](https://github.com/antfu) 341 | -------------------------------------------------------------------------------- /examples/preact-htm-browser/README.md: -------------------------------------------------------------------------------- 1 | # Preact + HTM + Vue Composition API 2 | 3 | 6 | 7 | This is an example browser app for [Reactivue](https://github.com/antfu/reactivue) library. 8 | 9 | Reactivue makes Vue Compostion API and Vue plugins work in Preact apps. 10 | 11 | [🚀 Interactive Demo](https://esm.codes/#aW1wb3J0IHsgaCwgcmVuZGVyIH0gZnJvbSAiaHR0cHM6Ly9jZG4uc2t5cGFjay5kZXYvcHJlYWN0IjsKaW1wb3J0IHsgdXNlU3RhdGUgfSBmcm9tICJodHRwczovL2Nkbi5za3lwYWNrLmRldi9wcmVhY3QvaG9va3MiOwppbXBvcnQgaHRtIGZyb20gImh0dHBzOi8vY2RuLnNreXBhY2suZGV2L2h0bSI7CmltcG9ydCB7CiAgZGVmaW5lQ29tcG9uZW50LAogIHJlZiwKICBjb21wdXRlZCwKICBvbk1vdW50ZWQsCiAgb25Vbm1vdW50ZWQsCn0gZnJvbSAiaHR0cHM6Ly9jZG4uc2t5cGFjay5kZXYvcmVhY3RpdnVlL3ByZWFjdCI7Cgpjb25zdCBodG1sID0gaHRtLmJpbmQoaCk7Cgpjb25zdCBNeUNvdW50ZXIgPSBkZWZpbmVDb21wb25lbnQoCiAgLy8gc2V0dXAgZnVuY3Rpb24gaW4gVnVlCiAgKHByb3BzKSA9PiB7CiAgICBjb25zdCBjb3VudGVyID0gcmVmKHByb3BzLnZhbHVlKTsKICAgIGNvbnN0IGRvdWJsZWQgPSBjb21wdXRlZCgoKSA9PiBjb3VudGVyLnZhbHVlICogMik7CiAgICBjb25zdCBpbmMgPSAoKSA9PiAoY291bnRlci52YWx1ZSArPSAxKTsKCiAgICBvbk1vdW50ZWQoKCkgPT4gY29uc29sZS5sb2coIkNvdW50ZXIgbW91bnRlZCIpKTsKICAgIG9uVW5tb3VudGVkKCgpID0+IGNvbnNvbGUubG9nKCJDb3VudGVyIHVubW91bnRlZCIpKTsKCiAgICByZXR1cm4geyBjb3VudGVyLCBkb3VibGVkLCBpbmMgfTsKICB9LAogIC8vIGZ1bmN0aW9uYWwgY29tcG9uZW50IGluIHByZWFjdAogICh7IGNvdW50ZXIsIGRvdWJsZWQsIGluYyB9KSA9PiB7CiAgICAvLyB5b3UgY2FuIHVzZSBwcmVhY3QgaG9va3MgaGVyZQogICAgcmV0dXJuIGh0bWxgCiAgICAgIDxkaXY+CiAgICAgICAgPGRpdj4ke2NvdW50ZXJ9IHggMiA9ICR7ZG91YmxlZH08L2Rpdj4KICAgICAgICA8YnV0dG9uIG9uQ2xpY2s9JHtpbmN9PkluY3JlYXNlPC9idXR0b24+CiAgICAgIDwvZGl2PgogICAgYDsKICB9Cik7CgpmdW5jdGlvbiBBcHAoKSB7CiAgY29uc3QgW3Nob3csIHNldFNob3ddID0gdXNlU3RhdGUodHJ1ZSk7CgogIHJldHVybiBodG1sYAogICAgPGJ1dHRvbiBvbkNsaWNrPSR7KCkgPT4gc2V0U2hvdyghc2hvdyl9PgogICAgICAke3Nob3cgPyAiSGlkZSIgOiAiU2hvdyJ9IGNvdW50ZXIKICAgIDwvYnV0dG9uPgogICAgJHtzaG93ICYmIGh0bWxgPCR7TXlDb3VudGVyfSB2YWx1ZT0kezEwfSAvPmB9CiAgYAp9CgpyZW5kZXIoaHRtbGA8JHtBcHB9Lz5gLCBkb2N1bWVudC5ib2R5KTsKCmRvY3VtZW50LmJvZHkuYmdDb2xvciA9ICJ3aGl0ZSI=) 12 | 13 | ## How to use 14 | 15 | ```bash 16 | npx degit antfu/reactivue/examples/preact-htm-browser 17 | cd preact-htm-browser 18 | npx serve 19 | ``` 20 | -------------------------------------------------------------------------------- /examples/preact-htm-browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Reactivue + Preact + htm 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/preact-htm-browser/script.js: -------------------------------------------------------------------------------- 1 | import { h, render } from "//cdn.skypack.dev/preact"; 2 | import { useState } from "//cdn.skypack.dev/preact/hooks"; 3 | import htm from "//cdn.skypack.dev/htm"; 4 | import { 5 | defineComponent, 6 | ref, 7 | computed, 8 | onMounted, 9 | onUnmounted, 10 | } from "//cdn.skypack.dev/reactivue/preact"; 11 | 12 | const html = htm.bind(h); 13 | 14 | const MyCounter = defineComponent( 15 | // setup function in Vue 16 | (props) => { 17 | const counter = ref(props.value); 18 | const doubled = computed(() => counter.value * 2); 19 | const inc = () => (counter.value += 1); 20 | 21 | onMounted(() => console.log("Counter mounted")); 22 | onUnmounted(() => console.log("Counter unmounted")); 23 | 24 | return { counter, doubled, inc }; 25 | }, 26 | // functional component in preact 27 | ({ counter, doubled, inc }) => { 28 | // you can use preact hooks here 29 | return html` 30 |
31 |
${counter} x 2 = ${doubled}
32 | 33 |
34 | `; 35 | } 36 | ); 37 | 38 | function App() { 39 | const [show, setShow] = useState(true); 40 | 41 | return html` 42 | 45 | ${show && html`<${MyCounter} value=${10} />`} 46 | ` 47 | } 48 | 49 | render(html`<${App}/>`, document.body); 50 | -------------------------------------------------------------------------------- /examples/react-ts-vite/README.md: -------------------------------------------------------------------------------- 1 | # React + Vue Composition API + Vite 2.0 2 | 3 | 6 | 7 | This is a demo app for [Reactivue](https://github.com/antfu/reactivue) library. Reactivue makes Vue Compostion API and Vue plugins work in React apps. 8 | 9 | ## How to use 10 | 11 | ```bash 12 | npx degit antfu/reactivue/examples/react-ts-vite 13 | cd react-ts-vite 14 | yarn install 15 | yarn dev 16 | ``` 17 | -------------------------------------------------------------------------------- /examples/react-ts-vite/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Reactivue Demo on Vite 7 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /examples/react-ts-vite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactivue-example-vite", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "serve": "vite preview" 9 | }, 10 | "dependencies": { 11 | "@vueuse/core": "^4.6.2", 12 | "@vueuse/head": "^0.5.1", 13 | "autoprefixer": "^10.2.5", 14 | "graphql": "^15.4.0", 15 | "pinia": "^2.0.0-alpha.8", 16 | "postcss": "^8.2.8", 17 | "react": "^17.0.2", 18 | "react-dom": "^17.0.2", 19 | "reactivue": "^0", 20 | "tailwindcss": "^2.0.4", 21 | "villus": "^1.0.0-rc.15" 22 | }, 23 | "devDependencies": { 24 | "@types/react": "^17.0.3", 25 | "@types/react-dom": "^17.0.3", 26 | "@vitejs/plugin-react-refresh": "1.3.1", 27 | "@vue/composition-api": "^1.0.0-rc.6", 28 | "typescript": "^4.2.3", 29 | "vite": "^2.1.4" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/react-ts-vite/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('tailwindcss'), 4 | require('autoprefixer'), 5 | ], 6 | } 7 | -------------------------------------------------------------------------------- /examples/react-ts-vite/src/basic/createSetup.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createSetup, ref, computed, onUnmounted } from 'reactivue' 3 | 4 | interface Props { 5 | value: number 6 | } 7 | 8 | const useMySetup = createSetup( 9 | (props: Props) => { 10 | const counter = ref(props.value) 11 | const doubled = computed(() => counter.value * 2) 12 | const inc = () => counter.value += 1 13 | 14 | onUnmounted(() => console.log('Goodbye World')) 15 | 16 | return { counter, doubled, inc } 17 | }, 18 | ) 19 | 20 | export const Counter = (props: Props) => { 21 | const { counter, doubled, inc } = useMySetup(props) 22 | const { counter: counter2, doubled: doubled2, inc: inc2 } = useMySetup({ value: 10 }) 23 | 24 | return ( 25 |
26 |

createSetup()

27 |
{counter} x 2 = {doubled}
28 | 29 |
{counter2} x 2 = {doubled2}
30 | 31 |
32 |
33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /examples/react-ts-vite/src/basic/defineComponent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { defineComponent, ref, computed, watch, onUnmounted, onMounted } from 'reactivue' 3 | 4 | export const Counter = defineComponent( 5 | (props: { value: number }) => { 6 | const counter = ref(props.value) 7 | 8 | const inc = () => (counter.value += 1) 9 | const dec = () => (counter.value -= 1) 10 | const doubled = computed(() => counter.value * 2) 11 | const isFive = ref(false) 12 | 13 | watch( 14 | () => props.value, 15 | v => (counter.value = v), 16 | ) 17 | watch(counter, v => (isFive.value = v === 5), { immediate: true }) 18 | 19 | onMounted(() => { 20 | console.log('Hello World.') 21 | }) 22 | 23 | onUnmounted(() => { 24 | console.log('Goodbye World.') 25 | }) 26 | 27 | return { counter, inc, dec, doubled, isFive } 28 | }, 29 | ({ counter, inc, doubled, dec, isFive }) => { 30 | return ( 31 |
32 |

defineComponent()

33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |
Counter{counter}
isFive{JSON.stringify(isFive)}
Doubled{doubled}
51 |
52 | ) 53 | }) 54 | -------------------------------------------------------------------------------- /examples/react-ts-vite/src/basic/useSetup.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import { useSetup, ref, computed, watch } from 'reactivue' 4 | 5 | export function Counter(Props: { value: number }) { 6 | const state = useSetup( 7 | (props) => { 8 | const counter = ref(props.value) 9 | 10 | const inc = () => (counter.value += 1) 11 | const dec = () => (counter.value -= 1) 12 | const doubled = computed(() => counter.value * 2) 13 | const isFive = ref(false) 14 | 15 | watch(() => props.value, v => (counter.value = v)) 16 | watch(counter, v => (isFive.value = v === 5), { immediate: true }) 17 | 18 | return { counter, inc, dec, doubled, isFive } 19 | }, 20 | Props, 21 | ) 22 | 23 | const { counter, inc, doubled, dec, isFive } = state 24 | return ( 25 |
26 |

useSetup()

27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
Counter{counter}
isFive{JSON.stringify(isFive)}
Doubled{doubled}
36 |
37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /examples/react-ts-vite/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import './main.css' 3 | import { render } from 'react-dom' 4 | import { createApp } from 'reactivue' 5 | import { createClient } from 'villus' 6 | import { createPinia } from 'pinia' 7 | import { createHead } from '@vueuse/head' 8 | import { Counter as Counter1 } from './basic/defineComponent' 9 | import { Counter as Counter2 } from './basic/useSetup' 10 | import { Counter as Counter3 } from './basic/createSetup' 11 | import { Pinia as Pinia1 } from './pinia/PiniaA' 12 | import { Pinia as Pinia2 } from './pinia/PiniaB' 13 | import { Mouse } from './vueuse/useMouse' 14 | import { Battery } from './vueuse/useBattery' 15 | import { Query } from './villus/useQuery' 16 | import { Head } from './vueuse-head/head' 17 | 18 | const app = createApp() 19 | 20 | app.use(createClient({ 21 | url: 'https://api.spacex.land/graphql/', 22 | })) 23 | 24 | app.use(createPinia()) 25 | app.use(createHead()) 26 | 27 | function App(Props: { name: string }) { 28 | const [c, s] = useState(0) 29 | const [show, setShow] = useState(true) 30 | return ( 31 |
32 |
33 | s(c + 1)}>reactivue demo 34 |
35 | 36 | 37 | 38 |

Basic

39 |
40 | { show ? : null} 41 | 42 | 43 |
44 | 45 |

Pinia 📎

46 |
47 | 48 | 49 |
50 | 51 |

VueUse 📎

52 |
53 | 54 | 55 |
56 | 57 |

@vueuse/head 📎

58 |
59 | 60 |
61 | 62 |

Villus 📎

63 |
64 | 65 |
66 |
67 | ) 68 | } 69 | 70 | render(, document.getElementById('app')) 71 | -------------------------------------------------------------------------------- /examples/react-ts-vite/src/main.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss/base'; 2 | @import 'tailwindcss/components'; 3 | @import 'tailwindcss/utilities'; 4 | 5 | .card { 6 | @apply bg-white p-3 m-1 rounded bg-opacity-10; 7 | font-feature-settings: "tnum"; 8 | font-variant-numeric: tabular-nums; 9 | } 10 | 11 | .card p { 12 | @apply text-gray-300 font-mono leading-none pb-4 mx-1 pt-1 opacity-50 text-sm; 13 | } 14 | 15 | button { 16 | @apply bg-white bg-opacity-10 px-4 py-1 rounded m-1 opacity-50; 17 | } 18 | 19 | button:hover { 20 | @apply opacity-75; 21 | } 22 | 23 | button:active { 24 | @apply opacity-100; 25 | } 26 | 27 | button:focus { 28 | @apply outline-none 29 | } 30 | 31 | table { 32 | @apply m-1 leading-7 w-full; 33 | } 34 | 35 | td:first-child { 36 | @apply text-right opacity-50 text-sm; 37 | } 38 | 39 | 40 | h2 { 41 | @apply mt-3 ml-1 text-lg; 42 | } 43 | 44 | input { 45 | @apply px-3 py-2 w-64 bg-white bg-opacity-10 border border-black border-opacity-25 rounded 46 | } -------------------------------------------------------------------------------- /examples/react-ts-vite/src/pinia/PiniaA.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import { defineComponent } from 'reactivue' 4 | import { useMainStore } from './store' 5 | 6 | export const Pinia = defineComponent( 7 | () => { 8 | const main = useMainStore() 9 | return main 10 | }, 11 | ({ $patch, counter, doubleCountPlusOne }) => { 12 | return ( 13 |
14 |

Pinia A

15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
Counter{counter}
Doubled + 1{doubleCountPlusOne}
25 |
26 | ) 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /examples/react-ts-vite/src/pinia/PiniaB.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import { defineComponent } from 'reactivue' 4 | import { useMainStore } from './store' 5 | 6 | export const Pinia = defineComponent( 7 | () => useMainStore(), 8 | ({ $patch, counter, doubleCount }) => { 9 | return ( 10 |
11 |

Pinia B

12 | 15 | 16 | 17 | 18 | 19 | 20 |
Counter{counter}
Doubled{doubleCount}
21 |
22 | ) 23 | }, 24 | ) 25 | -------------------------------------------------------------------------------- /examples/react-ts-vite/src/pinia/store.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | 3 | export const useMainStore = defineStore({ 4 | // name of the store 5 | // it is used in devtools and allows restoring state 6 | id: 'main', 7 | // a function that returns a fresh state 8 | state: () => ({ 9 | counter: 0, 10 | name: 'Eduardo', 11 | }), 12 | // optional getters 13 | getters: { 14 | doubleCount() { 15 | return this.counter * 2 16 | }, 17 | // use getters in other getters 18 | doubleCountPlusOne() { 19 | return this.doubleCount * 2 20 | }, 21 | }, 22 | // optional actions 23 | actions: { 24 | reset() { 25 | // `this` is the store instance 26 | this.counter = 0 27 | }, 28 | }, 29 | }) 30 | -------------------------------------------------------------------------------- /examples/react-ts-vite/src/villus/useQuery.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import { defineComponent } from 'reactivue' 4 | import { useQuery } from 'villus' 5 | const Posts = ` 6 | { 7 | company { 8 | founded 9 | name 10 | } 11 | } 12 | ` 13 | 14 | export const Query = defineComponent( 15 | () => { 16 | // useClient({ 17 | // url: 'https://api.spacex.land/graphql/', // your endpoint. 18 | // }) 19 | 20 | // without variables 21 | const { data } = useQuery({ 22 | query: Posts, 23 | }) 24 | 25 | return { data } 26 | }, 27 | ({ data }) => { 28 | return ( 29 |
30 |

useQuery

31 |
{ !data ? 'Loading' : JSON.stringify(data, null, 2) }
32 |
33 | ) 34 | }, 35 | ) 36 | -------------------------------------------------------------------------------- /examples/react-ts-vite/src/vueuse-head/head.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import { defineComponent, ref } from 'reactivue' 4 | import { useHead } from '@vueuse/head' 5 | 6 | export const Head = defineComponent( 7 | () => { 8 | const title = ref(document.title) 9 | const setTitle = (val: string) => title.value = val 10 | 11 | useHead({ title }) 12 | 13 | return { title, setTitle } 14 | }, 15 | ({ title, setTitle }) => { 16 | return ( 17 |
18 |

useHead

19 |
20 | setTitle(e.target.value)} /> 21 |
22 |
23 | ) 24 | }, 25 | ) 26 | -------------------------------------------------------------------------------- /examples/react-ts-vite/src/vueuse/useBattery.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import { defineComponent } from 'reactivue' 4 | import { useBattery } from '@vueuse/core' 5 | 6 | export const Battery = defineComponent( 7 | () => { 8 | return useBattery() 9 | }, 10 | ({ level, charging }) => { 11 | return ( 12 |
13 |

useBattery

14 |
Battery: {Math.round(level * 100)}% {charging ? '⚡️' : ''}
15 |
16 | ) 17 | }, 18 | ) 19 | -------------------------------------------------------------------------------- /examples/react-ts-vite/src/vueuse/useMouse.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import { defineComponent } from 'reactivue' 4 | import { useMouse } from '@vueuse/core' 5 | 6 | export const Mouse = defineComponent( 7 | () => { 8 | return useMouse() 9 | }, 10 | ({ x, y }) => { 11 | return ( 12 |
13 |

useMouse

14 |
{x} x {y}
15 |
16 | ) 17 | }, 18 | ) 19 | -------------------------------------------------------------------------------- /examples/react-ts-vite/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: { 3 | content: [ 4 | './index.html', 5 | './src/**/*.tsx', 6 | ], 7 | }, 8 | theme: { 9 | extend: {}, 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /examples/react-ts-vite/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 5 | "types": [], 6 | "allowJs": false, 7 | "skipLibCheck": false, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react", 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /examples/react-ts-vite/vite.config.ts: -------------------------------------------------------------------------------- 1 | import ReactRefresh from '@vitejs/plugin-react-refresh' 2 | import { defineConfig } from 'vite' 3 | 4 | export default defineConfig({ 5 | plugins: [ 6 | ReactRefresh(), 7 | ], 8 | resolve: { 9 | alias: { 10 | vue: 'reactivue', 11 | '@vue/composition-api': 'reactivue', 12 | '@vue/runtime-dom': 'reactivue', 13 | }, 14 | }, 15 | /** 16 | * Actually listing `reactivue` here is not required. 17 | * It's only required for our yarn workspaces setup. 18 | * For some reason Vite don't optimizes locally linked deps. 19 | */ 20 | optimizeDeps: { 21 | include: ['reactivue'], 22 | }, 23 | }) 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@reactivue/monorepo", 3 | "description": "Using Vue Composition API in React components.", 4 | "author": "Anthony Fu", 5 | "license": "MIT", 6 | "private": true, 7 | "workspaces": [ 8 | "packages/*", 9 | "examples/*" 10 | ], 11 | "scripts": { 12 | "dev": "npm -C ./packages/reactivue run dev", 13 | "dev:vite": "npm -C ./examples/react-ts-vite run dev", 14 | "build:vite": "npm -C ./examples/react-ts-vite run build", 15 | "serve:vite": "npm -C ./examples/react-ts-vite run serve", 16 | "build": "npm -C ./packages/reactivue run build && cpy README.md ./packages/reactivue", 17 | "release": "cd ./packages/reactivue && npm run build && npx bumpp --commit --tag --push && yarn publish --non-interactive", 18 | "postinstall": "yarn-deduplicate || exit 0", 19 | "test": "npm -C ./packages/reactivue run test" 20 | }, 21 | "devDependencies": { 22 | "@antfu/eslint-config-ts": "^0.6.2", 23 | "cpy-cli": "^3.1.1", 24 | "eslint": "^7.23.0", 25 | "eslint-plugin-react": "^7.23.1", 26 | "yarn-deduplicate": "^3.1.0" 27 | }, 28 | "eslintConfig": { 29 | "extends": [ 30 | "@antfu/eslint-config-ts", 31 | "plugin:react/recommended" 32 | ], 33 | "rules": { 34 | "react/prop-types": "off", 35 | "no-use-before-define": "off" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/reactivue/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-present, Anthony Fu 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. -------------------------------------------------------------------------------- /packages/reactivue/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {presets: ['@babel/preset-env']} 2 | -------------------------------------------------------------------------------- /packages/reactivue/global.d.ts: -------------------------------------------------------------------------------- 1 | declare let __DEV__: boolean 2 | declare let __BROWSER__: boolean 3 | -------------------------------------------------------------------------------- /packages/reactivue/jest.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | clearMocks: true, 3 | coverageProvider: 'v8', 4 | preset: 'ts-jest', 5 | transform: { 6 | '^.+\\.(ts|tsx)?$': 'ts-jest', 7 | }, 8 | setupFilesAfterEnv: ['./setupTests.ts'], 9 | globals: { 10 | 'ts-jest': { 11 | tsconfig: 'tsconfig.json', 12 | }, 13 | __DEV__: true, 14 | __BROWSER__: true, 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /packages/reactivue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactivue", 3 | "description": "Using Vue Composition API in React components.", 4 | "author": "Anthony Fu", 5 | "license": "MIT", 6 | "version": "0.4.4", 7 | "browser": "dist/index.module.js", 8 | "module": "dist/index.mjs", 9 | "main": "dist/index.js", 10 | "types": "dist/index.d.ts", 11 | "exports": { 12 | ".": { 13 | "types": "./dist/index.d.ts", 14 | "browser": { 15 | "production": "./dist/index.module.js", 16 | "development": "./dist/index.module.dev.js" 17 | }, 18 | "import": "./dist/index.mjs", 19 | "require": "./dist/index.js" 20 | }, 21 | "./": "./", 22 | "./preact": { 23 | "types": "./dist/index.d.ts", 24 | "browser": { 25 | "production": "./preact/index.module.js", 26 | "development": "./preact/index.module.dev.js" 27 | }, 28 | "import": "./preact/index.mjs", 29 | "require": "./preact/index.js" 30 | } 31 | }, 32 | "files": [ 33 | "dist", 34 | "preact" 35 | ], 36 | "sideEffects": false, 37 | "repository": { 38 | "type": "git", 39 | "url": "git+https://github.com/antfu/reactivue.git" 40 | }, 41 | "keywords": [ 42 | "vue", 43 | "react", 44 | "vue-composition-api", 45 | "vue-in-react", 46 | "reactivity" 47 | ], 48 | "bugs": { 49 | "url": "https://github.com/antfu/reactivue/issues" 50 | }, 51 | "homepage": "https://github.com/antfu/reactivue#readme", 52 | "scripts": { 53 | "dev": "rollup -c -w", 54 | "build": "rollup -c", 55 | "test": "jest" 56 | }, 57 | "devDependencies": { 58 | "@babel/preset-env": "^7.13.12", 59 | "@rollup/plugin-node-resolve": "^11.2.1", 60 | "@rollup/plugin-replace": "^2.4.2", 61 | "@testing-library/jest-dom": "^5.11.10", 62 | "@testing-library/react": "^11.2.6", 63 | "@types/jest": "^26.0.22", 64 | "@types/react": "^17.0.3", 65 | "@types/react-dom": "^17.0.3", 66 | "babel-jest": "^26.6.3", 67 | "cac": "^6.7.2", 68 | "jest": "^26.6.3", 69 | "preact": "^10.5.13", 70 | "react": "^17.0.2", 71 | "react-dom": "^17.0.2", 72 | "rollup": "^2.44.0", 73 | "rollup-plugin-dts": "^3.0.1", 74 | "rollup-plugin-terser": "^7.0.2", 75 | "rollup-plugin-typescript2": "^0.30.0", 76 | "ts-jest": "^26.5.4", 77 | "ts-node": "^9.1.1", 78 | "tslib": "^2.1.0", 79 | "typescript": "^4.2.3" 80 | }, 81 | "peerDependencies": { 82 | "preact": "^10.5.9", 83 | "react": ">=16" 84 | }, 85 | "dependencies": { 86 | "@vue/reactivity": "^3.0.9", 87 | "@vue/runtime-core": "^3.0.9", 88 | "@vue/shared": "^3.0.9" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /packages/reactivue/rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2' 2 | import dts from 'rollup-plugin-dts' 3 | import resolve from '@rollup/plugin-node-resolve' 4 | import replace from '@rollup/plugin-replace' 5 | import { terser } from 'rollup-plugin-terser' 6 | 7 | const external = [ 8 | '@vue/reactivity', 9 | '@vue/shared', 10 | '@vue/runtime-core', 11 | 'react', 12 | 'preact/hooks', 13 | ] 14 | 15 | const __DEV__ = '(process.env.NODE_ENV === \'development\')' 16 | const __BROWSER__ = '(typeof window !== \'undefined\')' 17 | 18 | const onwarn = (msg, warn) => !/Circular|preventAssignment/.test(msg) && warn(msg) 19 | 20 | export default [ 21 | { 22 | input: 'src/index.ts', 23 | output: [ 24 | { 25 | file: 'dist/index.js', 26 | format: 'cjs', 27 | }, 28 | ], 29 | plugins: [replace({ __DEV__, __BROWSER__ }), resolve(), typescript()], 30 | external, 31 | onwarn, 32 | }, 33 | { 34 | input: 'src/index.ts', 35 | output: [ 36 | { 37 | file: 'preact/index.js', 38 | format: 'cjs', 39 | }, 40 | ], 41 | plugins: [replace({ react: 'preact/hooks', __DEV__, __BROWSER__ }), resolve(), typescript()], 42 | external, 43 | onwarn, 44 | }, 45 | { 46 | input: 'src/index.ts', 47 | output: [ 48 | { 49 | file: 'dist/index.mjs', 50 | format: 'esm', 51 | }, 52 | ], 53 | plugins: [replace({ __DEV__, __BROWSER__ }), resolve(), typescript()], 54 | external, 55 | onwarn, 56 | }, 57 | { 58 | input: 'src/index.ts', 59 | output: [ 60 | { 61 | file: 'preact/index.mjs', 62 | format: 'es', 63 | }, 64 | ], 65 | plugins: [replace({ react: 'preact/hooks', __DEV__, __BROWSER__ }), resolve(), typescript()], 66 | external, 67 | onwarn, 68 | }, 69 | { 70 | input: 'src/index.ts', 71 | output: [ 72 | { 73 | file: 'dist/index.module.js', 74 | format: 'es', 75 | }, 76 | ], 77 | plugins: [replace({ __DEV__: false, __BROWSER__: true }), resolve(), typescript(), terser()], 78 | external, 79 | onwarn, 80 | }, 81 | { 82 | input: 'src/index.ts', 83 | output: [ 84 | { 85 | file: 'preact/index.module.js', 86 | format: 'es', 87 | }, 88 | ], 89 | plugins: [replace({ react: 'preact/hooks', __DEV__: false, __BROWSER__: true }), resolve(), typescript(), terser()], 90 | external, 91 | onwarn, 92 | }, 93 | { 94 | input: 'src/index.ts', 95 | output: [ 96 | { 97 | file: 'dist/index.module.dev.js', 98 | format: 'es', 99 | }, 100 | ], 101 | plugins: [replace({ __DEV__, __BROWSER__ }), resolve(), typescript()], 102 | external, 103 | onwarn, 104 | }, 105 | { 106 | input: 'src/index.ts', 107 | output: [ 108 | { 109 | file: 'preact/index.module.dev.js', 110 | format: 'es', 111 | }, 112 | ], 113 | plugins: [replace({ react: 'preact/hooks', __DEV__, __BROWSER__ }), resolve(), typescript()], 114 | external, 115 | onwarn, 116 | }, 117 | { 118 | input: 'src/index.ts', 119 | output: [ 120 | { 121 | file: 'dist/index.d.ts', 122 | format: 'es', 123 | }, 124 | { 125 | file: 'preact/index.d.ts', 126 | format: 'es', 127 | }, 128 | ], 129 | plugins: [dts()], 130 | external, 131 | }, 132 | ] 133 | -------------------------------------------------------------------------------- /packages/reactivue/setupTests.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/extend-expect' 2 | -------------------------------------------------------------------------------- /packages/reactivue/src/component.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-mutable-exports */ 2 | import { Ref, ReactiveEffect, ref, stop } from '@vue/reactivity' 3 | import * as vueReactivity from '@vue/reactivity' 4 | import { invokeLifeCycle } from './lifecycle' 5 | import { InstanceStateMap, InternalInstanceState, LifecycleHooks, EffectScope } from './types' 6 | 7 | /** 8 | * When `reactivue` dependency gets updated during development 9 | * your build tool re-executes it and `_vueState` becomes its 10 | * initial state. Storing our reactive effects in `window.__reactivue_state` 11 | * and filling our `_vueState` with it. 12 | */ 13 | declare global { 14 | interface Window { 15 | __reactivue_state: InstanceStateMap 16 | __reactivue_id: number 17 | } 18 | } 19 | 20 | let _id = (__DEV__ && __BROWSER__ && window.__reactivue_id) || 0 21 | const _vueState: InstanceStateMap = (__DEV__ && __BROWSER__ && window.__reactivue_state) || {} 22 | if (__DEV__ && __BROWSER__) 23 | window.__reactivue_state = _vueState 24 | 25 | const effectScope: (detached?: boolean) => EffectScope = (vueReactivity as any)['effectScope'] 26 | export const usingEffectScope = typeof effectScope === 'function' 27 | 28 | export let currentInstance: InternalInstanceState | null = null 29 | export let currentInstanceId: number | null = null 30 | 31 | export const getNewInstanceId = () => { 32 | _id++ 33 | 34 | if (__DEV__ && __BROWSER__) 35 | window.__reactivue_id = _id 36 | 37 | return _id 38 | } 39 | 40 | export const getCurrentInstance = () => currentInstance 41 | export const setCurrentInstance = ( 42 | instance: InternalInstanceState | null, 43 | ) => { 44 | currentInstance = instance 45 | } 46 | 47 | export const setCurrentInstanceId = (id: number | null) => { 48 | currentInstanceId = id 49 | currentInstance = id != null ? (_vueState[id] || null) : null 50 | return currentInstance 51 | } 52 | export const createNewInstanceWithId = (id: number, props: any, data: Ref = ref(null)) => { 53 | const instance: InternalInstanceState = { 54 | _id: id, 55 | props, 56 | data, 57 | isMounted: false, 58 | isUnmounted: false, 59 | isUnmounting: false, 60 | hooks: {}, 61 | initialState: {}, 62 | provides: __BROWSER__ ? { ...window.__reactivue_context?.provides } : {}, 63 | scope: usingEffectScope ? effectScope() : null, 64 | } 65 | _vueState[id] = instance 66 | 67 | return instance 68 | } 69 | 70 | export const useInstanceScope = (id: number, cb: (instance: InternalInstanceState | null) => void) => { 71 | const prev = currentInstanceId 72 | const instance = setCurrentInstanceId(id) 73 | if (usingEffectScope) { 74 | if (!instance?.isUnmounted) instance?.scope?.run(() => cb(instance)) 75 | } 76 | else cb(instance) 77 | setCurrentInstanceId(prev) 78 | } 79 | 80 | const unmount = (id: number) => { 81 | invokeLifeCycle(LifecycleHooks.BEFORE_UNMOUNT, _vueState[id]) 82 | 83 | // unregister all the computed/watch effects 84 | for (const effect of _vueState[id].effects || []) 85 | stop(effect) 86 | 87 | invokeLifeCycle(LifecycleHooks.UNMOUNTED, _vueState[id]) 88 | if (usingEffectScope) _vueState[id].scope!.stop() 89 | _vueState[id].isUnmounted = true 90 | 91 | // release the ref 92 | delete _vueState[id] 93 | } 94 | 95 | export const unmountInstance = (id: number) => { 96 | if (!_vueState[id]) 97 | return 98 | 99 | _vueState[id].isUnmounting = true 100 | 101 | /** 102 | * Postpone unmounting on dev. So we can check setup values 103 | * in useSetup.ts after hmr updates. That will not be an issue 104 | * for really unmounting components. Because they are increasing 105 | * instance id unlike the hmr updated components. 106 | */ 107 | if (__DEV__) 108 | setTimeout(() => _vueState[id]?.isUnmounting && unmount(id), 0) 109 | else 110 | unmount(id) 111 | } 112 | 113 | // record effects created during a component's setup() so that they can be 114 | // stopped when the component unmounts 115 | export function recordInstanceBoundEffect(effect: ReactiveEffect) { 116 | if (currentInstance) { 117 | if (!currentInstance.effects) 118 | currentInstance.effects = [] 119 | currentInstance.effects.push(effect) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /packages/reactivue/src/computed.ts: -------------------------------------------------------------------------------- 1 | import { 2 | computed as _computed, 3 | ComputedRef, 4 | WritableComputedOptions, 5 | WritableComputedRef, 6 | ComputedGetter, 7 | } from '@vue/reactivity' 8 | import { recordInstanceBoundEffect, usingEffectScope } from './component' 9 | 10 | export function computed(getter: ComputedGetter): ComputedRef 11 | export function computed( 12 | options: WritableComputedOptions 13 | ): WritableComputedRef 14 | export function computed( 15 | getterOrOptions: ComputedGetter | WritableComputedOptions, 16 | ) { 17 | const c = _computed(getterOrOptions as any) 18 | if (!usingEffectScope) recordInstanceBoundEffect(c.effect) 19 | return c 20 | } 21 | -------------------------------------------------------------------------------- /packages/reactivue/src/createSetup.ts: -------------------------------------------------------------------------------- 1 | import { useSetup } from './useSetup' 2 | 3 | export const createSetup = (setupFn: (props: Props) => State) => (props?: Props) => useSetup(setupFn, props) 4 | -------------------------------------------------------------------------------- /packages/reactivue/src/defineComponent.ts: -------------------------------------------------------------------------------- 1 | import { UnwrapRef } from '@vue/reactivity' 2 | import { useSetup } from './useSetup' 3 | 4 | export function defineComponent( 5 | setupFunction: (props: PropsType) => State, 6 | renderFunction: (state: UnwrapRef) => JSX.Element, 7 | ): (props: PropsType) => JSX.Element { 8 | return (props: PropsType) => { 9 | const state = useSetup(setupFunction, props) 10 | return renderFunction(state) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/reactivue/src/errorHandling.ts: -------------------------------------------------------------------------------- 1 | import { isFunction, isPromise } from '@vue/shared' 2 | import { InternalInstanceState } from './types' 3 | 4 | export function callWithErrorHandling( 5 | fn: Function, 6 | instance: InternalInstanceState | null, 7 | type: string, 8 | args?: unknown[], 9 | ) { 10 | let res 11 | try { 12 | res = args ? fn(...args) : fn() 13 | } 14 | catch (err) { 15 | handleError(err, instance, type) 16 | } 17 | return res 18 | } 19 | 20 | export function callWithAsyncErrorHandling( 21 | fn: Function | Function[], 22 | instance: InternalInstanceState | null, 23 | type: string, 24 | args?: unknown[], 25 | ): any[] { 26 | if (isFunction(fn)) { 27 | const res = callWithErrorHandling(fn, instance, type, args) 28 | if (res && isPromise(res)) { 29 | res.catch((err) => { 30 | handleError(err, instance, type) 31 | }) 32 | } 33 | return res 34 | } 35 | 36 | const values = [] 37 | for (let i = 0; i < fn.length; i++) 38 | values.push(callWithAsyncErrorHandling(fn[i], instance, type, args)) 39 | 40 | return values 41 | } 42 | 43 | export function handleError( 44 | err: unknown, 45 | instance: InternalInstanceState | null, 46 | type: String, 47 | ) { 48 | console.error(new Error(`[reactivue:${instance}]: ${type}`)) 49 | console.error(err) 50 | } 51 | 52 | export function raise(message: string): never { 53 | throw createError(message) 54 | } 55 | 56 | export function warn(message: string) { 57 | console.warn(createError(message)) 58 | } 59 | 60 | export function createError(message: string) { 61 | return new Error(`[reactivue]: ${message}`) 62 | } 63 | -------------------------------------------------------------------------------- /packages/reactivue/src/index.ts: -------------------------------------------------------------------------------- 1 | export { useSetup } from './useSetup' 2 | export { defineComponent } from './defineComponent' 3 | export { watch, watchEffect } from './watch' 4 | export { computed } from './computed' 5 | export { createSetup } from './createSetup' 6 | export { getCurrentInstance } from './component' 7 | export { nextTick } from './nextTick' 8 | export { warn } from './errorHandling' 9 | export * from './mock' 10 | export { 11 | onMounted, 12 | onBeforeMount, 13 | onUnmounted, 14 | onUpdated, 15 | onBeforeUnmount, 16 | onBeforeUpdate, 17 | } from './lifecycle' 18 | 19 | // redirect all APIs from @vue/reactivity 20 | export { 21 | // computed, 22 | ComputedGetter, 23 | ComputedRef, 24 | ComputedSetter, 25 | customRef, 26 | DebuggerEvent, 27 | DeepReadonly, 28 | effect, 29 | enableTracking, 30 | isProxy, 31 | isReactive, 32 | isReadonly, 33 | isRef, 34 | ITERATE_KEY, 35 | markRaw, 36 | pauseTracking, 37 | reactive, 38 | ReactiveEffect, 39 | ReactiveEffectOptions, 40 | ReactiveFlags, 41 | readonly, 42 | ref, 43 | Ref, 44 | RefUnwrapBailTypes, 45 | resetTracking, 46 | shallowReactive, 47 | shallowReadonly, 48 | shallowRef, 49 | stop, 50 | toRaw, 51 | toRef, 52 | toRefs, 53 | ToRefs, 54 | track, 55 | TrackOpTypes, 56 | trigger, 57 | TriggerOpTypes, 58 | triggerRef, 59 | unref, 60 | UnwrapRef, 61 | WritableComputedOptions, 62 | WritableComputedRef, 63 | } from '@vue/reactivity' 64 | -------------------------------------------------------------------------------- /packages/reactivue/src/lifecycle.ts: -------------------------------------------------------------------------------- 1 | // ported from https://github.com/vuejs/vue-next/blob/master/packages/runtime-core/src/apiLifecycle.ts 2 | 3 | import { pauseTracking, resetTracking } from '@vue/reactivity' 4 | import { currentInstance, setCurrentInstance, useInstanceScope } from './component' 5 | import { warn, callWithAsyncErrorHandling } from './errorHandling' 6 | import { LifecycleHooks, InternalInstanceState } from './types' 7 | 8 | export function injectHook( 9 | type: LifecycleHooks, 10 | hook: Function & { __weh?: Function }, 11 | target: InternalInstanceState | null = currentInstance, 12 | prepend = false, 13 | ) { 14 | if (target) { 15 | const hooks = target.hooks[type] || (target.hooks[type] = []) 16 | // cache the error handling wrapper for injected hooks so the same hook 17 | // can be properly deduped by the scheduler. "__weh" stands for "with error 18 | // handling". 19 | const wrappedHook 20 | = hook.__weh 21 | || (hook.__weh = (...args: unknown[]) => { 22 | if (target.isUnmounted) 23 | return 24 | 25 | // disable tracking inside all lifecycle hooks 26 | // since they can potentially be called inside effects. 27 | pauseTracking() 28 | // Set currentInstance during hook invocation. 29 | // This assumes the hook does not synchronously trigger other hooks, which 30 | // can only be false when the user does something really funky. 31 | setCurrentInstance(target) 32 | const res = callWithAsyncErrorHandling(hook, target, type, args) 33 | setCurrentInstance(null) 34 | resetTracking() 35 | return res 36 | }) 37 | if (prepend) 38 | hooks.unshift(wrappedHook) 39 | 40 | else if (!target.isMounted) 41 | hooks.push(wrappedHook) 42 | } 43 | else if (__DEV__) { 44 | warn( 45 | `on${type} is called when there is no active component instance to be ` 46 | + 'associated with. ' 47 | + 'Lifecycle injection APIs can only be used during execution of setup()', 48 | ) 49 | } 50 | } 51 | 52 | export const createHook = any>( 53 | lifecycle: LifecycleHooks, 54 | ) => (hook: T, target: InternalInstanceState | null = currentInstance) => injectHook(lifecycle, hook, target) 55 | 56 | export const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT) 57 | export const onMounted = createHook(LifecycleHooks.MOUNTED) 58 | export const onBeforeUpdate = createHook(LifecycleHooks.BEFORE_UPDATE) 59 | export const onUpdated = createHook(LifecycleHooks.UPDATED) 60 | export const onBeforeUnmount = createHook(LifecycleHooks.BEFORE_UNMOUNT) 61 | export const onUnmounted = createHook(LifecycleHooks.UNMOUNTED) 62 | 63 | export const invokeLifeCycle = ( 64 | type: LifecycleHooks, 65 | target: InternalInstanceState | null = currentInstance, 66 | ) => { 67 | if (target) { 68 | const hooks = target.hooks[type] || [] 69 | useInstanceScope(target._id, () => { 70 | for (const hook of hooks) 71 | hook() 72 | }) 73 | } 74 | else if (__DEV__) { 75 | warn(`on${type} is invoked without instance.`) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /packages/reactivue/src/mock.ts: -------------------------------------------------------------------------------- 1 | import { InjectionKey } from '@vue/runtime-core' 2 | import { isFunction } from '@vue/shared' 3 | import { getCurrentInstance } from './component' 4 | import { warn } from './errorHandling' 5 | 6 | type PluginInstallFunction = (app: AppContext['app'], ...options: any[]) => any 7 | 8 | export type Plugin = 9 | | PluginInstallFunction & { install?: PluginInstallFunction } 10 | | { 11 | install: PluginInstallFunction 12 | } 13 | 14 | declare global { 15 | interface Window { 16 | __reactivue_context: AppContext 17 | } 18 | } 19 | 20 | interface AppContext { 21 | app: any // for devtools 22 | config: { 23 | globalProperties: Record 24 | } 25 | provides: Record 26 | 27 | // not supported 28 | mixins: any 29 | components: any 30 | directives: any 31 | } 32 | 33 | function createAppContext(): AppContext { 34 | return { 35 | app: null as any, 36 | config: { 37 | globalProperties: {}, 38 | }, 39 | provides: {}, 40 | 41 | // not supported 42 | mixins: [], 43 | components: {}, 44 | directives: {}, 45 | } 46 | } 47 | 48 | export function createApp() { 49 | const context 50 | = __BROWSER__ && window.__reactivue_context 51 | ? window.__reactivue_context 52 | : createAppContext() 53 | 54 | if (__BROWSER__) 55 | window.__reactivue_context = context 56 | 57 | const installedPlugins = new Set() 58 | 59 | const app = (context.app = { 60 | version: '3.0.0', 61 | 62 | get config() { 63 | return context.config 64 | }, 65 | 66 | set config(_v) { 67 | if (__DEV__) { 68 | warn( 69 | 'app.config cannot be replaced. Modify individual options instead.', 70 | ) 71 | } 72 | }, 73 | 74 | use(plugin: Plugin, ...options: any[]) { 75 | if (installedPlugins.has(plugin)) { 76 | __DEV__ && warn('Plugin has already been applied to target app.') 77 | } 78 | else if (plugin && isFunction(plugin.install)) { 79 | installedPlugins.add(plugin) 80 | plugin.install(app, ...options) 81 | } 82 | else if (isFunction(plugin)) { 83 | installedPlugins.add(plugin) 84 | plugin(app, ...options) 85 | } 86 | else if (__DEV__) { 87 | warn( 88 | 'A plugin must either be a function or an object with an "install" ' 89 | + 'function.', 90 | ) 91 | } 92 | 93 | return app 94 | }, 95 | 96 | provide(key: InjectionKey | string, value: T) { 97 | if (__DEV__ && (key as string | symbol) in context.provides) { 98 | warn( 99 | `App already provides property with key "${String(key)}". ` 100 | + 'It will be overwritten with the new value.', 101 | ) 102 | } 103 | // TypeScript doesn't allow symbols as index type 104 | // https://github.com/Microsoft/TypeScript/issues/24587 105 | context.provides[key as string] = value 106 | }, 107 | 108 | mixin() { 109 | if (__DEV__) 110 | warn('`app.mixin` method is not supported in reactivue.') 111 | }, 112 | 113 | component() { 114 | if (__DEV__) 115 | warn('`app.component` method is not supported in reactivue.') 116 | }, 117 | 118 | directive() { 119 | if (__DEV__) 120 | warn('`app.directive` method is not supported in reactivue.') 121 | }, 122 | }) 123 | 124 | return app 125 | } 126 | export function h() { 127 | } 128 | 129 | export function provide(key: InjectionKey | string | number, value: T) { 130 | const instance = getCurrentInstance() 131 | 132 | if (instance) 133 | instance.provides[key as string] = value 134 | } 135 | 136 | export function inject(key: InjectionKey | string): T | undefined 137 | export function inject( 138 | key: InjectionKey | string, 139 | defaultValue: T, 140 | treatDefaultAsFactory?: false 141 | ): T 142 | export function inject( 143 | key: InjectionKey | string, 144 | defaultValue: T | (() => T), 145 | treatDefaultAsFactory: true 146 | ): T 147 | export function inject( 148 | key: InjectionKey | string, 149 | defaultValue?: unknown, 150 | treatDefaultAsFactory = false, 151 | ) { 152 | const instance = getCurrentInstance() 153 | 154 | if (instance) { 155 | if (instance.provides && (key as string | symbol) in instance.provides) { 156 | // TS doesn't allow symbol as index type 157 | return instance.provides[key as string] 158 | } 159 | else if (arguments.length > 1) { 160 | return treatDefaultAsFactory && isFunction(defaultValue) 161 | ? defaultValue() 162 | : defaultValue 163 | } 164 | else if (__DEV__) { 165 | warn(`injection "${String(key)}" not found.`) 166 | } 167 | } 168 | else if (__DEV__) { 169 | warn('inject() can only be used inside setup() or functional components.') 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /packages/reactivue/src/nextTick.ts: -------------------------------------------------------------------------------- 1 | export function nextTick(fn?: Function) { 2 | return new Promise((resolve) => { 3 | setTimeout(() => { 4 | fn && fn() 5 | resolve() 6 | }, 0) 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /packages/reactivue/src/types.ts: -------------------------------------------------------------------------------- 1 | import { ReactiveEffect, Ref } from '@vue/reactivity' 2 | 3 | // from https://unpkg.com/@vue/reactivity@3.2.47/dist/reactivity.d.ts#L75-L89 4 | export declare class EffectScope { 5 | detached: boolean; 6 | /* Excluded from this release type: _active */ 7 | /* Excluded from this release type: effects */ 8 | /* Excluded from this release type: cleanups */ 9 | /* Excluded from this release type: parent */ 10 | /* Excluded from this release type: scopes */ 11 | /* Excluded from this release type: index */ 12 | constructor(detached?: boolean); 13 | get active(): boolean; 14 | run(fn: () => T): T | undefined; 15 | /* Excluded from this release type: on */ 16 | /* Excluded from this release type: off */ 17 | stop(fromParent?: boolean): void; 18 | } 19 | 20 | export const enum LifecycleHooks { 21 | BEFORE_CREATE = 'BeforeMount', 22 | CREATED = 'Created', 23 | BEFORE_MOUNT = 'BeforeMount', 24 | MOUNTED = 'Mounted', 25 | BEFORE_UPDATE = 'BeforeUpdate', 26 | UPDATED = 'Updated', 27 | BEFORE_UNMOUNT = 'BeforeUnmount', 28 | UNMOUNTED = 'Unmounted', 29 | } 30 | 31 | export interface InternalInstanceState { 32 | _id: number 33 | props: any 34 | data: Ref 35 | isMounted: boolean 36 | isUnmounted: boolean 37 | isUnmounting: boolean 38 | effects?: ReactiveEffect[] 39 | hooks: Record 40 | initialState: Record 41 | provides: Record 42 | scope: EffectScope | null 43 | } 44 | 45 | export type InstanceStateMap = Record 46 | -------------------------------------------------------------------------------- /packages/reactivue/src/useSetup.ts: -------------------------------------------------------------------------------- 1 | import { UnwrapRef, reactive, ref, readonly, unref } from '@vue/reactivity' 2 | import { useState, useEffect } from 'react' 3 | import { getNewInstanceId, createNewInstanceWithId, useInstanceScope, unmountInstance } from './component' 4 | import { watch } from './watch' 5 | import { invokeLifeCycle } from './lifecycle' 6 | import { LifecycleHooks } from './types' 7 | 8 | export function useSetup, Props = {}>( 9 | setupFunction: (props: Props) => State, 10 | ReactProps?: Props, 11 | ): UnwrapRef { 12 | const id = useState(getNewInstanceId)[0] 13 | 14 | const setTick = useState(0)[1] 15 | 16 | const createState = () => { 17 | const props = reactive({ ...(ReactProps || {}) }) as any 18 | const instance = createNewInstanceWithId(id, props) 19 | 20 | useInstanceScope(id, () => { 21 | const setupState = setupFunction(readonly(props)) ?? {} 22 | const data = ref(setupState) 23 | 24 | invokeLifeCycle(LifecycleHooks.BEFORE_MOUNT) 25 | 26 | instance.data = data 27 | 28 | if (__DEV__) { 29 | for (const key of Object.keys(setupState)) 30 | instance.initialState[key] = unref(setupState[key]) 31 | } 32 | }) 33 | 34 | return instance.data.value 35 | } 36 | 37 | // run setup function 38 | const [state, setState] = useState(createState) 39 | 40 | // sync props changes 41 | useEffect(() => { 42 | if (!ReactProps) return 43 | 44 | useInstanceScope(id, (instance) => { 45 | if (!instance) 46 | return 47 | const { props } = instance 48 | for (const key of Object.keys(ReactProps)) 49 | props[key] = (ReactProps as any)[key] 50 | }) 51 | }, [ReactProps]) 52 | 53 | // trigger React re-render on data changes 54 | useEffect(() => { 55 | /** 56 | * Invalidate setup after hmr updates 57 | */ 58 | if (__DEV__) { 59 | let isChanged = false 60 | 61 | useInstanceScope(id, (instance) => { 62 | if (!instance) 63 | return 64 | 65 | if (!instance.isUnmounting) 66 | return 67 | 68 | const props = Object.assign({}, (ReactProps || {})) as any 69 | const setup = setupFunction(readonly(props)) 70 | 71 | for (const key of Object.keys(setup)) { 72 | if (isChanged) 73 | break 74 | 75 | if (typeof instance.initialState[key] === 'function') 76 | isChanged = instance.initialState[key].toString() !== setup[key].toString() 77 | else 78 | isChanged = instance.initialState[key] !== unref(setup[key]) 79 | } 80 | 81 | instance.isUnmounting = false 82 | }) 83 | 84 | if (isChanged) 85 | setState(createState()) 86 | } 87 | 88 | useInstanceScope(id, (instance) => { 89 | if (!instance) 90 | return 91 | 92 | // Avoid repeated execution of onMounted and watch after hmr updates in development mode 93 | if (instance.isMounted) 94 | return 95 | 96 | instance.isMounted = true 97 | 98 | invokeLifeCycle(LifecycleHooks.MOUNTED) 99 | 100 | const { data } = instance 101 | watch( 102 | data, 103 | () => { 104 | /** 105 | * Prevent triggering rerender when component 106 | * is about to unmount or really unmounted 107 | */ 108 | if (instance.isUnmounting) 109 | return 110 | 111 | useInstanceScope(id, () => { 112 | invokeLifeCycle(LifecycleHooks.BEFORE_UPDATE, instance) 113 | // trigger React update 114 | setTick(+new Date()) 115 | invokeLifeCycle(LifecycleHooks.UPDATED, instance) 116 | }) 117 | }, 118 | { deep: true, flush: 'post' }, 119 | ) 120 | }) 121 | 122 | return () => { 123 | unmountInstance(id) 124 | } 125 | }, []) 126 | 127 | return state 128 | } 129 | -------------------------------------------------------------------------------- /packages/reactivue/src/watch.ts: -------------------------------------------------------------------------------- 1 | // ported from https://github.com/vuejs/vue-next/blob/master/packages/runtime-core/src/apiWatch.ts 2 | 3 | /* eslint-disable array-callback-return */ 4 | import { effect, Ref, ComputedRef, ReactiveEffectOptions, isRef, isReactive, stop } from '@vue/reactivity' 5 | import { isFunction, isArray, NOOP, isObject, remove, hasChanged } from '@vue/shared' 6 | import { watch as _watch, watchEffect as _watchEffect } from '@vue/runtime-core' 7 | import { currentInstance, recordInstanceBoundEffect, usingEffectScope } from './component' 8 | import { warn, callWithErrorHandling, callWithAsyncErrorHandling } from './errorHandling' 9 | 10 | export type WatchEffect = (onInvalidate: InvalidateCbRegistrator) => void 11 | 12 | export type WatchSource = Ref | ComputedRef | (() => T) 13 | 14 | export type WatchCallback = ( 15 | value: V, 16 | oldValue: OV, 17 | onInvalidate: InvalidateCbRegistrator 18 | ) => any 19 | 20 | export type WatchStopHandle = () => void 21 | 22 | type MapSources = { 23 | [K in keyof T]: T[K] extends WatchSource 24 | ? V 25 | : T[K] extends object ? T[K] : never 26 | } 27 | 28 | type MapOldSources = { 29 | [K in keyof T]: T[K] extends WatchSource 30 | ? Immediate extends true ? (V | undefined) : V 31 | : T[K] extends object 32 | ? Immediate extends true ? (T[K] | undefined) : T[K] 33 | : never 34 | } 35 | 36 | type InvalidateCbRegistrator = (cb: () => void) => void 37 | const INITIAL_WATCHER_VALUE = {} 38 | 39 | export interface WatchOptionsBase { 40 | flush?: 'pre' | 'post' | 'sync' 41 | onTrack?: ReactiveEffectOptions['onTrack'] 42 | onTrigger?: ReactiveEffectOptions['onTrigger'] 43 | } 44 | 45 | export interface WatchOptions extends WatchOptionsBase { 46 | immediate?: Immediate 47 | deep?: boolean 48 | } 49 | 50 | // Simple effect. 51 | export function watchEffect( 52 | effect: WatchEffect, 53 | options?: WatchOptionsBase, 54 | ): WatchStopHandle { 55 | if (usingEffectScope) return _watchEffect(effect, options) 56 | return doWatch(effect, null, options) 57 | } 58 | 59 | // overload #1: array of multiple sources + cb 60 | // Readonly constraint helps the callback to correctly infer value types based 61 | // on position in the source array. Otherwise the values will get a union type 62 | // of all possible value types. 63 | export function watch< 64 | T extends Readonly | object>>, 65 | Immediate extends Readonly = false 66 | >( 67 | sources: T, 68 | cb: WatchCallback, MapOldSources>, 69 | options?: WatchOptions 70 | ): WatchStopHandle 71 | 72 | // overload #2: single source + cb 73 | export function watch = false>( 74 | source: WatchSource, 75 | cb: WatchCallback, 76 | options?: WatchOptions 77 | ): WatchStopHandle 78 | 79 | // overload #3: watching reactive object w/ cb 80 | export function watch< 81 | T extends object, 82 | Immediate extends Readonly = false 83 | >( 84 | source: T, 85 | cb: WatchCallback, 86 | options?: WatchOptions 87 | ): WatchStopHandle 88 | 89 | // implementation 90 | export function watch( 91 | source: WatchSource | WatchSource[], 92 | cb: WatchCallback, 93 | options?: WatchOptions, 94 | ): WatchStopHandle { 95 | if (usingEffectScope) return _watch(source, cb as any, options) 96 | return doWatch(source, cb, options) 97 | } 98 | 99 | function doWatch( 100 | source: WatchSource | WatchSource[] | WatchEffect, 101 | cb: WatchCallback | null, 102 | { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = {}, 103 | ): WatchStopHandle { 104 | const instance = currentInstance 105 | 106 | let getter: () => any 107 | let forceTrigger = false 108 | let isMultiSource = false 109 | if (isRef(source)) { 110 | getter = () => (source as Ref).value 111 | // @ts-expect-error 112 | forceTrigger = !!(source as Ref)._shallow 113 | } 114 | else if (isReactive(source)) { 115 | getter = () => source 116 | deep = true 117 | } 118 | else if (isArray(source)) { 119 | isMultiSource = true 120 | forceTrigger = source.some(isReactive) 121 | getter = () => 122 | source.map((s) => { 123 | if (isRef(s)) 124 | return s.value 125 | else if (isReactive(s)) 126 | return traverse(s) 127 | else if (isFunction(s)) 128 | return callWithErrorHandling(s, instance, 'watch getter') 129 | else 130 | __DEV__ && warn('invalid source') 131 | }) 132 | } 133 | else if (isFunction(source)) { 134 | if (cb) { 135 | // getter with cb 136 | getter = () => 137 | callWithErrorHandling(source, instance, 'watch getter') 138 | } 139 | else { 140 | // no cb -> simple effect 141 | getter = () => { 142 | if (instance && instance.isUnmounted) 143 | return 144 | 145 | if (cleanup) 146 | cleanup() 147 | 148 | return callWithErrorHandling( 149 | source, 150 | instance, 151 | 'watch callback', 152 | [onInvalidate], 153 | ) 154 | } 155 | } 156 | } 157 | else { 158 | getter = NOOP 159 | } 160 | 161 | if (cb && deep) { 162 | const baseGetter = getter 163 | getter = () => traverse(baseGetter()) 164 | } 165 | 166 | let cleanup: () => void 167 | const onInvalidate: InvalidateCbRegistrator = (fn: () => void) => { 168 | cleanup = runner.options.onStop = () => { 169 | callWithErrorHandling(fn, instance, 'watch cleanup') 170 | } 171 | } 172 | 173 | let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE 174 | const job = () => { 175 | if (!runner.active) 176 | return 177 | 178 | if (cb) { 179 | // watch(source, cb) 180 | const newValue = runner() 181 | if ( 182 | deep 183 | || forceTrigger 184 | || (isMultiSource 185 | ? (newValue as any[]).some((v, i) => 186 | hasChanged(v, (oldValue as any[])[i]), 187 | ) 188 | : hasChanged(newValue, oldValue)) 189 | ) { 190 | // cleanup before running cb again 191 | if (cleanup) 192 | cleanup() 193 | 194 | callWithAsyncErrorHandling(cb, instance, 'watch callback', [ 195 | newValue, 196 | // pass undefined as the old value when it's changed for the first time 197 | oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue, 198 | onInvalidate, 199 | ]) 200 | oldValue = newValue 201 | } 202 | } 203 | else { 204 | // watchEffect 205 | runner() 206 | } 207 | } 208 | 209 | // important: mark the job as a watcher callback so that scheduler knows 210 | // it is allowed to self-trigger (#1727) 211 | job.allowRecurse = !!cb 212 | 213 | let scheduler: ReactiveEffectOptions['scheduler'] 214 | if (flush === 'sync') { 215 | scheduler = job 216 | } 217 | else if (flush === 'post') { 218 | scheduler = () => job() 219 | // TODO: scheduler = () => queuePostRenderEffect(job, instance && instance.suspense) 220 | } 221 | else { 222 | // default: 'pre' 223 | scheduler = () => { 224 | if (!instance) { 225 | // TODO: queuePreFlushCb(job) 226 | job() 227 | } 228 | else { 229 | // with 'pre' option, the first call must happen before 230 | // the component is mounted so it is called synchronously. 231 | job() 232 | } 233 | } 234 | } 235 | 236 | const runner = effect(getter, { 237 | lazy: true, 238 | onTrack, 239 | onTrigger, 240 | scheduler, 241 | }) 242 | 243 | recordInstanceBoundEffect(runner) 244 | 245 | // initial run 246 | if (cb) { 247 | if (immediate) 248 | job() 249 | else 250 | oldValue = runner() 251 | } 252 | else { 253 | runner() 254 | } 255 | 256 | return () => { 257 | stop(runner) 258 | if (instance) 259 | remove(instance.effects!, runner) 260 | } 261 | } 262 | 263 | function traverse(value: unknown, seen: Set = new Set()) { 264 | if (!isObject(value) || seen.has(value)) 265 | return value 266 | 267 | seen.add(value) 268 | if (isArray(value)) { 269 | for (let i = 0; i < value.length; i++) 270 | traverse(value[i], seen) 271 | } 272 | else if (value instanceof Map) { 273 | value.forEach((_, key) => { 274 | // to register mutation dep for existing keys 275 | traverse(value.get(key), seen) 276 | }) 277 | } 278 | else if (value instanceof Set) { 279 | value.forEach((v) => { 280 | traverse(v, seen) 281 | }) 282 | } 283 | else { 284 | for (const key of Object.keys(value)) 285 | traverse(value[key], seen) 286 | } 287 | return value 288 | } 289 | -------------------------------------------------------------------------------- /packages/reactivue/tests/computed.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from '@testing-library/react' 3 | import { screen, waitFor } from '@testing-library/dom' 4 | import { useSetup, toRef, computed } from '../src' 5 | 6 | const ComputedTest = (Props: { hello: string }) => { 7 | const { comp } = useSetup((props) => { 8 | const other = toRef(props, 'hello') 9 | 10 | const comp = computed(() => `${other?.value?.substr(0, 5) || ''}, Universe!`) 11 | 12 | return { 13 | comp, 14 | } 15 | }, Props) 16 | 17 | return

{comp}

18 | } 19 | 20 | it('should handle computed properties', async() => { 21 | const comp = render() 22 | 23 | await waitFor(() => { 24 | const el = screen.getByText('Hello, Universe!') 25 | expect(el).toBeInTheDocument() 26 | }) 27 | 28 | comp.rerender() 29 | 30 | await waitFor(() => { 31 | const el = screen.getByText('Adios, Universe!') 32 | expect(el).toBeInTheDocument() 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /packages/reactivue/tests/createSetup.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from '@testing-library/react' 3 | import { screen, waitFor } from '@testing-library/dom' 4 | import { createSetup, ref } from '../src' 5 | 6 | const useMsg = createSetup(() => { 7 | const msg = ref('Hello, world!') 8 | 9 | return { 10 | msg, 11 | } 12 | }) 13 | 14 | const CreateSetupTest = () => { 15 | const { msg } = useMsg({}) 16 | return

{msg}

17 | } 18 | 19 | it('should render basic createSetup return', async() => { 20 | render() 21 | 22 | await waitFor(() => { 23 | const el = screen.getByText('Hello, world!') 24 | expect(el).toBeInTheDocument() 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /packages/reactivue/tests/defineComponent.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from '@testing-library/react' 3 | import { screen, waitFor } from '@testing-library/dom' 4 | import { defineComponent, ref } from '../src' 5 | 6 | const DefineTest = defineComponent(() => { 7 | const msg = ref('Hello, world!') 8 | 9 | return { 10 | msg, 11 | } 12 | }, ({ msg }) => { 13 | return

{msg}

14 | }) 15 | 16 | it('should render basic defineComponent component', async() => { 17 | render() 18 | 19 | await waitFor(() => { 20 | const el = screen.getByText('Hello, world!') 21 | expect(el).toBeInTheDocument() 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /packages/reactivue/tests/lifecycle.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { fireEvent, render } from '@testing-library/react' 3 | import { screen, waitFor } from '@testing-library/dom' 4 | import { 5 | useSetup, 6 | ref, 7 | onBeforeMount, 8 | onMounted, 9 | onBeforeUpdate, 10 | onUpdated, 11 | onBeforeUnmount, 12 | onUnmounted, 13 | } from '../src' 14 | 15 | const onMountedJestFn = jest.fn() 16 | const onBeforeMountJestFn = jest.fn() 17 | const onUnmountedJestFn = jest.fn() 18 | const onUpdatedJestFn = jest.fn() 19 | const onBeforeUnmountJestFn = jest.fn() 20 | const onBeforeUpdateJestFn = jest.fn() 21 | 22 | beforeEach(() => { 23 | onMountedJestFn.mockClear() 24 | onBeforeMountJestFn.mockClear() 25 | onUnmountedJestFn.mockClear() 26 | onUpdatedJestFn.mockClear() 27 | onBeforeUnmountJestFn.mockClear() 28 | onBeforeUpdateJestFn.mockClear() 29 | }) 30 | 31 | const LifecycleTest = () => { 32 | const { num, addOne } = useSetup(() => { 33 | const val = ref(0) 34 | 35 | const addToVal = () => val.value += 1 36 | 37 | onBeforeMount(() => onBeforeMountJestFn()) 38 | onMounted(() => onMountedJestFn()) 39 | onBeforeUpdate(() => onBeforeUpdateJestFn()) 40 | onUpdated(() => onUpdatedJestFn()) 41 | onBeforeUnmount(() => onBeforeUnmountJestFn()) 42 | onUnmounted(() => onUnmountedJestFn()) 43 | 44 | return { 45 | num: val, 46 | addOne: addToVal, 47 | } 48 | }, {}) 49 | 50 | return
51 |

{num}

52 | 53 |
54 | } 55 | 56 | it('should handle mount lifecycles', async() => { 57 | render() 58 | 59 | await waitFor(() => { 60 | expect(onBeforeMountJestFn).toBeCalled() 61 | expect(onMountedJestFn).toBeCalled() 62 | }) 63 | }) 64 | 65 | it('should handle update lifecycles', async() => { 66 | render() 67 | 68 | fireEvent.click(screen.getByText('Add one')) 69 | 70 | await waitFor(() => { 71 | expect(onBeforeUpdateJestFn).toBeCalled() 72 | expect(onUpdatedJestFn).toBeCalled() 73 | }) 74 | }) 75 | 76 | it('should handle unmount lifecycles', async() => { 77 | const comp = render() 78 | 79 | comp.unmount() 80 | 81 | await waitFor(() => { 82 | expect(onBeforeUnmountJestFn).toBeCalled() 83 | expect(onUnmountedJestFn).toBeCalled() 84 | }) 85 | }) 86 | -------------------------------------------------------------------------------- /packages/reactivue/tests/mock.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from '@testing-library/react' 3 | import { screen } from '@testing-library/dom' 4 | import { inject, provide, useSetup } from '../src' 5 | 6 | const ChildComp = (Props: { }) => { 7 | const { message } = useSetup(() => { 8 | const message = inject('key', 'Hello, world') 9 | 10 | return { 11 | message, 12 | } 13 | }, Props) 14 | 15 | return

{message}

16 | } 17 | 18 | const ParentComp = (Props: { }) => { 19 | useSetup(() => { 20 | provide('key', 'Hello, world') 21 | 22 | return {} 23 | }, Props) 24 | 25 | return 26 | } 27 | 28 | it('should handle computed properties', async() => { 29 | render() 30 | 31 | const el = screen.getByText('Hello, world') 32 | expect(el).toBeInTheDocument() 33 | }) 34 | -------------------------------------------------------------------------------- /packages/reactivue/tests/useSetup.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from '@testing-library/react' 3 | import { screen, waitFor } from '@testing-library/dom' 4 | import { useSetup, ref, toRef } from '../src' 5 | 6 | const SetupTest = (Props: { hello?: string }) => { 7 | const { msg, other } = useSetup((props) => { 8 | const msg = ref('Hello, world!') 9 | const other = toRef(props, 'hello') 10 | 11 | return { 12 | msg, 13 | other, 14 | } 15 | }, Props) 16 | 17 | return
18 |

{msg}

19 |

{other || ''}

20 |
21 | } 22 | 23 | it('should render basic useSetup function return', async() => { 24 | render() 25 | 26 | await waitFor(() => { 27 | const el = screen.getByText('Hello, world!') 28 | expect(el).toBeInTheDocument() 29 | }) 30 | }) 31 | 32 | it('should render basic useSetup function return', async() => { 33 | render() 34 | 35 | await waitFor(() => { 36 | const el = screen.getByText('Hello, Universe!') 37 | expect(el).toBeInTheDocument() 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /packages/reactivue/tests/watch.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from '@testing-library/react' 3 | import { waitFor } from '@testing-library/dom' 4 | import { useSetup, toRef, watch, watchEffect } from '../src' 5 | 6 | const watchJestFn = jest.fn() 7 | const watchEffectJestFn = jest.fn() 8 | 9 | beforeEach(() => { 10 | watchJestFn.mockClear() 11 | watchEffectJestFn.mockClear() 12 | }) 13 | 14 | const WatchTest = (Props: { hello: string }) => { 15 | const { other } = useSetup((props) => { 16 | const other = toRef(props, 'hello') 17 | 18 | watch(other, () => watchJestFn()) 19 | 20 | watchEffect(() => { 21 | watchEffectJestFn(other.value) 22 | }) 23 | 24 | return { 25 | other, 26 | } 27 | }, Props) 28 | 29 | return

{other}

30 | } 31 | 32 | it('should handle watch ref', async() => { 33 | const { rerender } = render() 34 | 35 | rerender() 36 | 37 | await waitFor(() => { 38 | expect(watchJestFn).toBeCalled() 39 | }) 40 | }) 41 | 42 | it('should handle watchEffect ref', async() => { 43 | const comp = render() 44 | 45 | comp.rerender() 46 | 47 | await waitFor(() => { 48 | expect(watchEffectJestFn).toBeCalled() 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /packages/reactivue/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /screenshots/logo.svg: -------------------------------------------------------------------------------- 1 | Artboard 1 -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "target": "es2017", 5 | "lib": ["ESNext", "DOM"], 6 | "strict": true, 7 | "jsx": "react", 8 | "rootDir": ".", 9 | "esModuleInterop": true, 10 | "moduleResolution": "node", 11 | "skipLibCheck": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "noImplicitReturns": true 15 | }, 16 | "exclude": ["**/node_modules", "**/dist", "**/preact"] 17 | } 18 | --------------------------------------------------------------------------------