Repository: effector/effector (ts)

Issue:
Isomorphic `useUnit` in `effector-react`
## Proposal

`useUnit` from `effector-react` tries to find `scope` in the context and automatically choose correct mode:
- `scope`-full — binds events/effects to scope, reads store's values from scope
- `scope`-less — returns events/effects as is, reads global store's values 

Add optional parameter `forceScope` to imitate behaviour of current `useUnit` from `effector-react/scope`, throws error in `scope`-less case.

## Use case

Merge two packages and simplify `scope`-full cases in applications — SSR and tests.

Pull request:
Add forceScope parameter to useUnit in react
### Conventions
- [x] Please check your messages [against the guidelines](https://cbea.ms/git-commit/) if not, perform [interactive rebase](https://thoughtbot.com/blog/git-interactive-rebase-squash-amend-rewriting-history)
- [x] [Link an issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) your pull request closes or relates

Resolves #765

Patch:
diff --git a/README.md b/README.md
index 2f77c7e6..ffcf7dcf 100644
--- a/README.md
+++ b/README.md
@@ -229,9 +229,9 @@ Your support allows us to improve the developer experience 🧡.
     </td>
     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
         <a href=https://github.com/igorkamyshev>
-            <img src=https://avatars.githubusercontent.com/u/26767722?v=4 width="100;"  alt=Igor Kamyshev/>
+            <img src=https://avatars.githubusercontent.com/u/26767722?v=4 width="100;"  alt=Igor Kamyşev/>
             <br />
-            <sub style="font-size:14px"><b>Igor Kamyshev</b></sub>
+            <sub style="font-size:14px"><b>Igor Kamyşev</b></sub>
         </a>
     </td>
 </tr>
diff --git a/docs/api/effector/allSettled.md b/docs/api/effector/allSettled.md
index 52a65eee..b1c8d9d9 100644
--- a/docs/api/effector/allSettled.md
+++ b/docs/api/effector/allSettled.md
@@ -10,6 +10,7 @@ allSettled<T>(unit: Effect<T, Done, Fail>, {scope: Scope, params?: T}): Promise<
   | {status: 'done'; value: Done}
   | {status: 'fail'; value: Fail}
 >
+allSettled<T>(unit: Store<T>, {scope: Scope, params?: T}): Promise<void>
 ```
 
 Call provided unit in scope and wait for finishing all the triggered effects.
diff --git a/docs/api/effector/createWatch.md b/docs/api/effector/createWatch.md
index 685c4ec5..4ec4437c 100644
--- a/docs/api/effector/createWatch.md
+++ b/docs/api/effector/createWatch.md
@@ -27,7 +27,7 @@ createWatch<T>(config: {
 #### Example (scope)
 
 ```js
-import {createWatch, createEvent, fork, allSetteld} from 'effector'
+import {createWatch, createEvent, fork, allSettled} from 'effector'
 
 const changeName = createEvent()
 
@@ -42,7 +42,7 @@ changeName('John') // no output
 #### Example (no scope)
 
 ```js
-import {createWatch, createEvent, fork, allSetteld} from 'effector'
+import {createWatch, createEvent, fork, allSettled} from 'effector'
 
 const changeName = createEvent()
 
diff --git a/docs/introduction/community.md b/docs/introduction/community.md
index c7ec65ae..752d387a 100644
--- a/docs/introduction/community.md
+++ b/docs/introduction/community.md
@@ -43,12 +43,12 @@ description: Articles and the team behind the project
 
 <br /><br />
 
-# Meet the Effector Team
+## Meet the Effector Team
 
 The Effector team members work full time on the projects which use Effector to solve business tasks.
 Each member uses the library every day as a user and tries to improve the user experience as a core team member.
 
-## Dmitry Boldyrev
+### Dmitry Boldyrev
 
 <img width="256" src="https://avatars.githubusercontent.com/u/15912112?v=4" />
 
@@ -57,7 +57,7 @@ Each member uses the library every day as a user and tries to improve the user e
 Dmitry made the first version of effector in 2018 to solve reactive event-driver architecture in the messenger.
 Now his main focus is to improve UX in the effector itself and speed up the kernel.
 
-## Sergey Sova
+### Sergey Sova
 
 <img width="256" src="https://avatars.githubusercontent.com/u/5620073?v=4" />
 
@@ -66,7 +66,7 @@ Now his main focus is to improve UX in the effector itself and speed up the kern
 Since 2018 Sergey has made some ecosystem packages: [patronum](https://github.com/effector/patronum), [logger](https://github.com/effector/logger), [inspector](https://github.com/effector/inspector).
 His main task is to improve the UX through the ecosystem and documentation.
 
-## Alexandr Horoshih
+### Alexandr Horoshih
 
 <img width="256" src="https://avatars.githubusercontent.com/u/32790736?v=4" />
 
@@ -75,7 +75,7 @@ His main task is to improve the UX through the ecosystem and documentation.
 Alexander contributed to each package of effector core and org repository.
 He reviewed contributions and improved the DX of the core functionality.
 
-## Kirill Mironov
+### Kirill Mironov
 
 <img width="256" src="https://i.imgur.com/JFaZkm9.jpg" />
 
@@ -84,7 +84,7 @@ He reviewed contributions and improved the DX of the core functionality.
 Kirill made the [swc-plugin](https://github.com/effector/swc-plugin), the [bindings for SolidJS](https://github.com/effector/effector/tree/master/packages/effector-solid),
 and now improves ecosystem and core functionality.
 
-## Igor Kamyşev
+### Igor Kamyşev
 
 <img width="256" src="https://avatars.githubusercontent.com/u/26767722?v=4" />
 
@@ -93,7 +93,7 @@ and now improves ecosystem and core functionality.
 Igor is working on [Farfetched](https://farfetched.pages.dev) is the advanced data fetching tool.
 Igor made [eslint-plugin-effector](https://github.com/effector/eslint-plugin) and reviewed many of PRs and issues of the effector and ecosystem packages.
 
-## Yan Lobaty
+### Yan Lobaty
 
 <img width="256" src="https://i.imgur.com/DomL22D.jpg" />
 
@@ -102,7 +102,7 @@ Igor made [eslint-plugin-effector](https://github.com/effector/eslint-plugin) an
 Yan made many contributions with fixes and improvements to all effector repositories.
 Yan helps us to write explanations and reference documentation. You may hear about the workshop Yan made about effector.
 
-## Egor Guscha
+### Egor Guscha
 
 <img width="256" src="https://avatars.githubusercontent.com/u/22044607?v=4" />
 
@@ -112,9 +112,9 @@ Since 2019 working in the effector core team on documentation, learning material
 
 <br /><br />
 
-# Acknowledgments
+## Acknowledgments
 
-## Ilya Lesik
+### Ilya Lesik
 
 <img width="256" src="https://avatars.githubusercontent.com/u/1270648?v=4" />
 
@@ -122,7 +122,7 @@ Since 2019 working in the effector core team on documentation, learning material
 
 Ilya made the list of awesome packages of effector ecosystem.
 
-## Evgeniy Fedotov 
+### Evgeniy Fedotov 
 
 <img width="256" src="https://avatars.githubusercontent.com/u/18236014?v=4" />
 
@@ -130,7 +130,7 @@ Ilya made the list of awesome packages of effector ecosystem.
 
 Evgeniy made [effector-reflect](https://github.com/effector/reflect) and helps us write documentation.
 
-## Valeriy Kobzar
+### Valeriy Kobzar
 
 <img width="256" src="https://avatars.githubusercontent.com/u/1615093?v=4" />
 
@@ -138,7 +138,7 @@ Evgeniy made [effector-reflect](https://github.com/effector/reflect) and helps u
 
 Valeriy developed server-side code for [REPL](https://share.effector.dev) and wrote many documentation pages.
 
-## Anton Kosykh
+### Anton Kosykh
 
 <img width="256" src="https://i.imgur.com/GD0zWpH.jpg" />
 
@@ -148,13 +148,13 @@ One of the earliest users of Effector, working on [Atomic Router](https://atomic
 [effector-pagination](https://github.com/kelin2025/effector-pagination) and [effector-factorio](https://github.com/Kelin2025/effector-factorio)
 
 
-## Andrei Tshurotshkin
+### Andrei Tshurotshkin
 
 [Github](https://github.com/goodmind)
 
 Andrei was at the origin of the effector. He wrote all the first documentation, implemented the first REPL version, and featured many core methods.
 
-## Roman Titov 
+### Roman Titov 
 
 [Github](https://github.com/popuguytheparrot) • [Telegram](https://t.me/popuguy)
 
@@ -164,7 +164,7 @@ _This list is not exhaustive._
 
 <br /><br />
 
-# Contributors
+## Contributors
 
 Please, open [README.md](https://github.com/effector/effector#contributors) to see the full list of our contributors.
 We have the [github action](https://github.com/effector/effector/blob/master/.github/workflows/contributors.yml) to regenerate this list.
diff --git a/docs/typescript/typing-effector.md b/docs/typescript/typing-effector.md
index dfc9a342..75d1f956 100644
--- a/docs/typescript/typing-effector.md
+++ b/docs/typescript/typing-effector.md
@@ -135,6 +135,64 @@ const userMessage = guard(message, {
 // userMessage has type Event<UserMessage>
 ```
 
+## sample
+Since `effector@22.2.0` update `sample` also supports a `filter` field, which can also be a [Typescript type predicate](https://www.typescriptlang.org/docs/handbook/advanced-types.html#using-type-predicates).
+
+```typescript
+type UserMessage = {kind: 'user'; text: string}
+type WarnMessage = {kind: 'warn'; warn: string}
+
+const message = createEvent<UserMessage | WarnMessage>()
+const userMessage = createEvent<UserMessage>()
+
+sample({
+  clock: message,
+  filter: (msg): msg is UserMessage => msg.kind === 'user',
+  target: userMessage
+})
+```
+
+### filter + fn
+
+However, `sample` also has a `fn` field to apply custom transformations.
+There is a caveat with Typescript type inference mechanic, which requires user to explicitly type `filter` arguments for type inference to work
+
+```typescript
+type UserMessage = {kind: 'user'; text: string}
+type WarnMessage = {kind: 'warn'; warn: string}
+type Message = UserMessage | WarnMessage
+
+const message = createEvent<Message>()
+const userText = createEvent<string>()
+
+sample({
+  clock: message,
+  // need to explicitly type `msg` as `Message` there
+  filter: (msg: Message): msg is UserMessage => msg.kind === 'user',
+  // to get correct type inference here
+  fn: (msg) => msg.text,
+  target: userText,
+})
+
+// userMessage has type Event<string>
+```
+Otherwise Typescrit will fallback to `any`.
+
+However, Typescript will not allow you to set incorrect filter type
+```typescript
+const message = createEvent<Message>()
+const userMessage = createEvent<UserMessage>()
+
+sample({
+  clock: message,
+  // Type 'Message' is not assignable to type '{ kind: "user" | "wrong"; text: number; }'.
+  filter: (msg: {kind: 'user' | 'wrong'; text: number}): msg is UserMessage => msg.kind === 'user',
+  fn: (msg) => msg.text,
+  target: userMessage
+})
+```
+
+
 ## createApi
 
 To allow typescript to infer types of created events, add type to second argument of given reducers
diff --git a/packages/effector-react/index.d.ts b/packages/effector-react/index.d.ts
index 03fd7ff7..ace5f0e5 100644
--- a/packages/effector-react/index.d.ts
+++ b/packages/effector-react/index.d.ts
@@ -1,5 +1,7 @@
 import React from 'react'
-import {Store, Event, Effect, Domain} from 'effector'
+import {Store, Event, Effect, Domain, Scope} from 'effector'
+
+export const Provider: React.Provider<Scope>
 
 export type StoreConsumer<State> = React.ComponentType<{
   children: (state: State) => React.ReactNode
@@ -154,15 +156,31 @@ type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y
   ? true
   : false
 
-export function useUnit<State>(store: Store<State>): State
-export function useUnit(event: Event<void>): () => void
-export function useUnit<T>(event: Event<T>): (payload: T) => T
-export function useUnit<R>(fx: Effect<void, R, any>): () => Promise<R>
-export function useUnit<T, R>(fx: Effect<T, R, any>): (payload: T) => Promise<R>
+export function useUnit<State>(
+  store: Store<State>,
+  opts?: {forceScope?: boolean},
+): State
+export function useUnit(
+  event: Event<void>,
+  opts?: {forceScope?: boolean},
+): () => void
+export function useUnit<T>(
+  event: Event<T>,
+  opts?: {forceScope?: boolean},
+): (payload: T) => T
+export function useUnit<R>(
+  fx: Effect<void, R, any>,
+  opts?: {forceScope?: boolean},
+): () => Promise<R>
+export function useUnit<T, R>(
+  fx: Effect<T, R, any>,
+  opts?: {forceScope?: boolean},
+): (payload: T) => Promise<R>
 export function useUnit<
   List extends (Event<any> | Effect<any, any> | Store<any>)[],
 >(
   list: [...List],
+  opts?: {forceScope?: boolean},
 ): {
   [Key in keyof List]: List[Key] extends Event<infer T>
     ? Equal<T, void> extends true
@@ -180,6 +198,7 @@ export function useUnit<
   Shape extends Record<string, Event<any> | Effect<any, any, any> | Store<any>>,
 >(
   shape: Shape,
+  opts?: {forceScope?: boolean},
 ): {
   [Key in keyof Shape]: Shape[Key] extends Event<infer T>
     ? Equal<T, void> extends true
diff --git a/packages/effector-react/index.ts b/packages/effector-react/index.ts
index ae89788e..07f811c9 100644
--- a/packages/effector-react/index.ts
+++ b/packages/effector-react/index.ts
@@ -1,1 +1,2 @@
 export * from '../../src/react'
+export {Provider} from '../../src/react/scope'
diff --git a/packages/effector-react/scope.ts b/packages/effector-react/scope.ts
index ce667820..249f7ac9 100644
--- a/packages/effector-react/scope.ts
+++ b/packages/effector-react/scope.ts
@@ -1,1 +1,2 @@
 export * from '../../src/react/ssr'
+export {Provider} from '../../src/react/scope'
diff --git a/packages/effector/index.d.ts b/packages/effector/index.d.ts
index 29ad4e58..aeffecaa 100644
--- a/packages/effector/index.d.ts
+++ b/packages/effector/index.d.ts
@@ -3053,8 +3053,8 @@ export function allSettled<FX extends Effect<void, any, any>>(
   config: {scope: Scope},
 ): Promise<{status: 'done', value: EffectResult<FX>} | {status: 'fail'; value: EffectError<FX>}>
 /**
- * Run event in scope and wait for all triggered effects to settle. This method never throw an error
- * @param unit event to run
+ * Run unit in scope and wait for all triggered effects to settle. This method never throw an error
+ * @param unit event or store to run
  * @returns void promise, will resolve when there will be no pending effects in given scope
  */
 export function allSettled<T>(
@@ -3062,8 +3062,8 @@ export function allSettled<T>(
   config: {scope: Scope; params: T},
 ): Promise<void>
 /**
- * Run event withot arguments in scope and wait for all triggered effects to settle. This method never throw an error
- * @param unit event to run
+ * Run unit without arguments in scope and wait for all triggered effects to settle. This method never throw an error
+ * @param unit event or store to run
  * @returns void promise, will resolve when there will be no pending effects in given scope
  */
 export function allSettled(
diff --git a/src/react/nossr.ts b/src/react/nossr.ts
index 5876c7b2..6af072c4 100644
--- a/src/react/nossr.ts
+++ b/src/react/nossr.ts
@@ -5,6 +5,7 @@ import {
   useListBase,
   useUnitBase,
 } from './apiBase'
+import {getScope} from './scope'
 
 /**
 bind event to scope
@@ -19,8 +20,8 @@ export function useStore<State>(store: Store<State>): State {
   return useStoreBase(store)
 }
 
-export function useUnit(shape) {
-  return useUnitBase(shape)
+export function useUnit(shape, opts?: {forceScope?: boolean}) {
+  return useUnitBase(shape, getScope(opts?.forceScope))
 }
 
 export function useStoreMap<State, Result, Keys extends ReadonlyArray<any>>(
diff --git a/src/react/scope.ts b/src/react/scope.ts
new file mode 100644
index 00000000..2c21d6a8
--- /dev/null
+++ b/src/react/scope.ts
@@ -0,0 +1,12 @@
+import React from 'react'
+import {Scope} from 'effector'
+import {throwError} from './throw'
+
+const ScopeContext = React.createContext(null as Scope | null)
+export const {Provider} = ScopeContext
+export function getScope(forceScope?: boolean) {
+  const scope = React.useContext(ScopeContext)
+  if (forceScope && !scope)
+    throwError('No scope found, consider adding <Provider> to app root')
+  return scope as Scope
+}
diff --git a/src/react/ssr.ts b/src/react/ssr.ts
index a1c2bf94..6e086ea0 100644
--- a/src/react/ssr.ts
+++ b/src/react/ssr.ts
@@ -1,5 +1,5 @@
 import React from 'react'
-import {Domain, is, Scope, Store, scopeBind} from 'effector'
+import {Domain, is, Store, scopeBind} from 'effector'
 import {
   useStoreBase,
   useUnitBase,
@@ -15,15 +15,7 @@ import {
 import type {Gate} from './index.h'
 import {throwError} from './throw'
 import {deprecate} from './deprecate'
-
-const ScopeContext = React.createContext(null as Scope | null)
-export const {Provider} = ScopeContext
-function getScope() {
-  const scope = React.useContext(ScopeContext)
-  if (!scope)
-    throwError('No scope found, consider adding <Provider> to app root')
-  return scope as Scope
-}
+import {getScope} from './scope'
 
 export function createGate<Props>(
   ...args: Array<
diff --git a/website/client/docusaurus.config.js b/website/client/docusaurus.config.js
index 65baca44..93fc3e9e 100644
--- a/website/client/docusaurus.config.js
+++ b/website/client/docusaurus.config.js
@@ -69,7 +69,7 @@ module.exports = {
             },
             {
               label: 'API',
-              to: 'docs/api/effector/effector',
+              to: 'docs/api/effector',
             },
             {
               label: 'Changelog',
diff --git a/website/client/i18n/ru/docusaurus-plugin-content-docs/current/api/effector/allSettled.md b/website/client/i18n/ru/docusaurus-plugin-content-docs/current/api/effector/allSettled.md
index 52a65eee..b1c8d9d9 100644
--- a/website/client/i18n/ru/docusaurus-plugin-content-docs/current/api/effector/allSettled.md
+++ b/website/client/i18n/ru/docusaurus-plugin-content-docs/current/api/effector/allSettled.md
@@ -10,6 +10,7 @@ allSettled<T>(unit: Effect<T, Done, Fail>, {scope: Scope, params?: T}): Promise<
   | {status: 'done'; value: Done}
   | {status: 'fail'; value: Fail}
 >
+allSettled<T>(unit: Store<T>, {scope: Scope, params?: T}): Promise<void>
 ```
 
 Call provided unit in scope and wait for finishing all the triggered effects.
diff --git a/website/client/i18n/ru/docusaurus-plugin-content-docs/current/api/effector/createWatch.md b/website/client/i18n/ru/docusaurus-plugin-content-docs/current/api/effector/createWatch.md
index a52d7772..95ed0d9a 100644
--- a/website/client/i18n/ru/docusaurus-plugin-content-docs/current/api/effector/createWatch.md
+++ b/website/client/i18n/ru/docusaurus-plugin-content-docs/current/api/effector/createWatch.md
@@ -27,7 +27,7 @@ createWatch<T>(config: {
 #### Пример (со скоупом)
 
 ```js
-import {createWatch, createEvent, fork, allSetteld} from 'effector'
+import {createWatch, createEvent, fork, allSettled} from 'effector'
 
 const changeName = createEvent()
 
@@ -42,7 +42,7 @@ changeName('Иван') // no output
 #### Пример (без скоупа)
 
 ```js
-import {createWatch, createEvent, fork, allSetteld} from 'effector'
+import {createWatch, createEvent, fork, allSettled} from 'effector'
 
 const changeName = createEvent()
 


Test patch:
diff --git a/src/react/__tests__/base/useUnit.test.tsx b/src/react/__tests__/base/useUnit.test.tsx
index 599a4def..5ad0daf8 100644
--- a/src/react/__tests__/base/useUnit.test.tsx
+++ b/src/react/__tests__/base/useUnit.test.tsx
@@ -861,4 +861,139 @@ describe('useUnit', () => {
       `)
     })
   })
+  describe('useUnit with force scope', () => {
+    let consoleError: any
+    beforeEach(() => {
+      consoleError = console.error
+      console.error = () => {}
+    })
+    afterEach(() => {
+      console.error = consoleError
+    })
+    describe('forceScope options not set', () => {
+      it('without context', async () => {
+        const $a = createStore(42)
+
+        const View = () => {
+          const a = useUnit($a)
+
+          return <div>{a}</div>
+        }
+
+        expect(() =>
+          render(<View />).toMatchInlineSnapshot(`
+            <div>
+              42
+            </div>
+          `),
+        )
+      })
+      test('with context', async () => {
+        const $value = createStore('value')
+
+        const Component = () => {
+          const value = useUnit($value)
+          return <div>{value}</div>
+        }
+
+        const scope = fork({values: [[$value, 'scoped value']]})
+
+        await render(
+          <React.StrictMode>
+            <Provider value={scope}>
+              <Component />
+            </Provider>
+          </React.StrictMode>,
+        )
+
+        expect(container.firstChild).toMatchInlineSnapshot(`
+            <div>
+              scoped value
+            </div>
+          `)
+      })
+    })
+    describe('forceScope disabled', () => {
+      it('without context', async () => {
+        const $a = createStore(42)
+
+        const View = () => {
+          const a = useUnit($a, {forceScope: false})
+
+          return <div>{a}</div>
+        }
+
+        expect(() =>
+          render(<View />).toMatchInlineSnapshot(`
+            <div>
+              42
+            </div>
+          `),
+        )
+      })
+      test('with context', async () => {
+        const $value = createStore('value')
+
+        const Component = () => {
+          const value = useUnit($value, {forceScope: false})
+          return <div>{value}</div>
+        }
+
+        const scope = fork({values: [[$value, 'scoped value']]})
+
+        await render(
+          <React.StrictMode>
+            <Provider value={scope}>
+              <Component />
+            </Provider>
+          </React.StrictMode>,
+        )
+
+        expect(container.firstChild).toMatchInlineSnapshot(`
+            <div>
+              scoped value
+            </div>
+          `)
+      })
+    })
+    describe('forceScope enabled', () => {
+      it('without context', async () => {
+        const $a = createStore(42)
+
+        const View = () => {
+          const a = useUnit($a, {forceScope: true})
+
+          return <div>{a}</div>
+        }
+
+        expect(() => render(<View />)).rejects.toThrow(
+          'No scope found, consider adding <Provider> to app root',
+        )
+      })
+      test('with context', async () => {
+        const $value = createStore('value')
+
+        const Component = () => {
+          const value = useUnit($value, {forceScope: true})
+          return <div>{value}</div>
+        }
+
+        const scope = fork({values: [[$value, 'scoped value']]})
+
+        await render(
+          <React.StrictMode>
+            <Provider value={scope}>
+              <Component />
+            </Provider>
+          </React.StrictMode>,
+        )
+
+        expect(container.firstChild).toMatchInlineSnapshot(`
+            <div>
+              scoped value
+            </div>
+          `)
+      })
+    })
+  })
 })