18 |
--------------------------------------------------------------------------------
/apps/website/docs/i18next/change_language.md:
--------------------------------------------------------------------------------
1 | # `changeLanguageFx`
2 |
3 | An [_Effect_](https://effector.dev/en/api/effector/effect/) that can be called with a language code to change the current language.
4 |
5 | ```ts
6 | const { changeLanguageFx } = createI18nextIntegration({
7 | /* ... */
8 | });
9 |
10 | sample({
11 | clock: someButtonClicked,
12 | fn: () => 'en',
13 | target: changeLanguageFx,
14 | });
15 | ```
16 |
--------------------------------------------------------------------------------
/apps/web-api-demo/playwright.config.ts:
--------------------------------------------------------------------------------
1 | import type { PlaywrightTestConfig } from '@playwright/test';
2 |
3 | const config: PlaywrightTestConfig = {
4 | retries: 3,
5 | maxFailures: 2,
6 | timeout: 120000,
7 | use: { baseURL: 'http://localhost:5173' },
8 | webServer: {
9 | command: 'pnpm --filter web-api-demo dev',
10 | url: 'http://localhost:5173',
11 | reuseExistingServer: !process.env.CI,
12 | },
13 | };
14 |
15 | export default config;
16 |
--------------------------------------------------------------------------------
/apps/website/docs/statements/tests.md:
--------------------------------------------------------------------------------
1 | # Testing
2 |
3 | We believe that the only way to write good code is to test it. It leads to the following consequences:
4 |
5 | 1. Any feature in any library listed at With Ease are created with testing in mind, so users do not have to invent some complex ways to test their code.
6 | 2. Libraries are tested itself. It means that you can be sure that it works as expected. We are not accepting any pull requests that do not have tests.
7 |
--------------------------------------------------------------------------------
/tools/publint.mjs:
--------------------------------------------------------------------------------
1 | import { publint } from 'publint';
2 | import { formatMessage } from 'publint/utils';
3 | import fs from 'node:fs/promises';
4 |
5 | const { messages } = await publint({
6 | pkgDir: '.',
7 | strict: true,
8 | });
9 |
10 | if (messages.length) {
11 | const pkg = await fs.readFile('package.json', 'utf8').then(JSON.parse);
12 |
13 | for (const message of messages) {
14 | console.log(formatMessage(message, pkg));
15 | }
16 | process.exit(1);
17 | }
18 |
--------------------------------------------------------------------------------
/packages/contracts/vite.config.js:
--------------------------------------------------------------------------------
1 | import tsconfigPaths from 'vite-tsconfig-paths';
2 | import dts from '../../tools/vite/types';
3 |
4 | export default {
5 | test: {
6 | typecheck: {
7 | ignoreSourceErrors: true,
8 | },
9 | },
10 | plugins: [tsconfigPaths(), dts()],
11 | build: {
12 | lib: {
13 | entry: 'src/index.ts',
14 | name: '@withease/contracts',
15 | fileName: 'contracts',
16 | formats: ['es', 'cjs'],
17 | },
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/packages/factories/vite.config.js:
--------------------------------------------------------------------------------
1 | import tsconfigPaths from 'vite-tsconfig-paths';
2 | import dts from '../../tools/vite/types';
3 |
4 | export default {
5 | test: {
6 | typecheck: {
7 | ignoreSourceErrors: true,
8 | },
9 | },
10 | plugins: [tsconfigPaths(), dts()],
11 | build: {
12 | lib: {
13 | entry: 'src/index.ts',
14 | name: '@withease/factories',
15 | fileName: 'factories',
16 | formats: ['es', 'cjs'],
17 | },
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/apps/web-api-demo/src/preferred-languages.ts:
--------------------------------------------------------------------------------
1 | import { trackPreferredLanguages } from '@withease/web-api';
2 | import { createEvent } from 'effector';
3 |
4 | const appStarted = createEvent();
5 |
6 | const languageElement = document.querySelector('#language')!;
7 |
8 | const { $language } = trackPreferredLanguages({ setup: appStarted });
9 |
10 | $language.watch((language) => {
11 | console.log('language', language);
12 | languageElement.textContent = language;
13 | });
14 |
15 | appStarted();
16 |
--------------------------------------------------------------------------------
/packages/factories/src/errors.ts:
--------------------------------------------------------------------------------
1 | export function factoryCalledDirectly() {
2 | return new Error(
3 | 'Do not call factory directly, pass it to invoke function instead'
4 | );
5 | }
6 |
7 | export function invokeAcceptsOnlyFactories() {
8 | return new Error('Function passed to invoke is not created by createFactory');
9 | }
10 |
11 | export function factoryHasMoreThanOneArgument() {
12 | return new Error(
13 | 'createFactory does not support functions with more than 1 argument'
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/apps/web-api-demo/test/preferred_languages.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from '@playwright/test';
2 |
3 | const PREFERRED_LANGUAGES_PAGE = '/preferred-languages.html';
4 |
5 | test.use({ locale: 'it-IT' });
6 |
7 | test('should detect initial language', async ({ page }) => {
8 | await page.goto(PREFERRED_LANGUAGES_PAGE);
9 |
10 | const languageContainer = await page.$('#language');
11 | const languageTextContent = await languageContainer.textContent();
12 |
13 | expect(languageTextContent).toBe('it-IT');
14 | });
15 |
--------------------------------------------------------------------------------
/.changeset/README.md:
--------------------------------------------------------------------------------
1 | # Changesets
2 |
3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4 | with multi-package repos, or single-package repos to help you version and publish your code. You can
5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6 |
7 | We have a quick list of common questions to get you started engaging with this project in
8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
9 |
--------------------------------------------------------------------------------
/apps/website/docs/i18next/reporting.md:
--------------------------------------------------------------------------------
1 | # `reporting`
2 |
3 | An object with the following fields:
4 |
5 | - `missingKey`, [_Event_](https://effector.dev/en/api/effector/event/) will be triggered when a key is missing in the translation resources, requires [adding `saveMissing` option to the i18next instance](https://www.i18next.com/overview/api#onmissingkey).
6 |
7 | ```ts
8 | const { reporting } = createI18nextIntegration({
9 | /* ... */
10 | });
11 |
12 | sample({ clock: reporting.missingKey, target: sendWarningToSentry });
13 | ```
14 |
--------------------------------------------------------------------------------
/.github/workflows/changes.yml:
--------------------------------------------------------------------------------
1 | name: Changes
2 |
3 | on:
4 | pull_request:
5 | branches: [master, next-*]
6 |
7 | jobs:
8 | changeset:
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - uses: actions/checkout@v3
13 | with:
14 | fetch-depth: 0
15 | - uses: pnpm/action-setup@v2
16 | - uses: actions/setup-node@v3
17 | with:
18 | node-version-file: '.nvmrc'
19 | cache: 'pnpm'
20 | - run: pnpm install --frozen-lockfile
21 | - run: pnpm pnpm changeset status
22 |
--------------------------------------------------------------------------------
/packages/web-api/vite.config.js:
--------------------------------------------------------------------------------
1 | import tsconfigPaths from 'vite-tsconfig-paths';
2 | import dts from '../../tools/vite/types';
3 |
4 | export default {
5 | test: {
6 | typecheck: {
7 | ignoreSourceErrors: true,
8 | },
9 | },
10 | plugins: [tsconfigPaths(), dts()],
11 | build: {
12 | lib: {
13 | entry: 'src/index.ts',
14 | name: '@withease/web-api',
15 | fileName: 'web-api',
16 | formats: ['es', 'cjs'],
17 | },
18 | rollupOptions: {
19 | external: ['effector'],
20 | },
21 | },
22 | };
23 |
--------------------------------------------------------------------------------
/packages/redux/vite.config.js:
--------------------------------------------------------------------------------
1 | import tsconfigPaths from 'vite-tsconfig-paths';
2 | import dts from '../../tools/vite/types';
3 |
4 | export default {
5 | test: {
6 | typecheck: {
7 | ignoreSourceErrors: true,
8 | },
9 | },
10 | plugins: [tsconfigPaths(), dts()],
11 | build: {
12 | lib: {
13 | entry: 'src/index.ts',
14 | name: '@withease/redux',
15 | fileName: 'redux',
16 | formats: ['es', 'cjs'],
17 | },
18 | rollupOptions: {
19 | external: ['effector', 'redux'],
20 | },
21 | },
22 | };
23 |
--------------------------------------------------------------------------------
/apps/website/docs/statements/typescript.md:
--------------------------------------------------------------------------------
1 | # TypeScript
2 |
3 | All libraries listen at With Ease are going to provide first-class support of TypeScript types. They are written in TypeScript, and includes type-tests. However, TypeScript itself does not aim for [apply a sound or "provably correct" type system](https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals#non-goals), instead it strikes a balance between correctness and productivity. So, we cannot guarantee that all the types are correct, but we are going to do our best to provide the best possible types.
4 |
--------------------------------------------------------------------------------
/packages/i18next/vite.config.js:
--------------------------------------------------------------------------------
1 | import tsconfigPaths from 'vite-tsconfig-paths';
2 | import dts from '../../tools/vite/types';
3 |
4 | export default {
5 | test: {
6 | typecheck: {
7 | ignoreSourceErrors: true,
8 | },
9 | },
10 | plugins: [tsconfigPaths(), dts()],
11 | build: {
12 | lib: {
13 | entry: 'src/index.ts',
14 | name: '@withease/i18next',
15 | fileName: 'i18next',
16 | formats: ['es', 'cjs'],
17 | },
18 | rollupOptions: {
19 | external: ['effector', 'i18next'],
20 | },
21 | },
22 | };
23 |
--------------------------------------------------------------------------------
/apps/website/docs/web-api/geolocation.live.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
latitude: {{ latitude }}
16 |
longitude: {{ longitude }}
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/apps/website/docs/i18next/releases.md:
--------------------------------------------------------------------------------
1 | # Releases policy of `@withease/i18next`
2 |
3 | ::: tip
4 | This library does not follow the same [release cycle as other libraries in the With Ease organization](/statements/releases) because it is bound to the release cycle of `i18next` itself.
5 | :::
6 |
7 | The major version of this library is bound to the major version of `i18next` itself. For example, `@withease/i18next@22.x.x` is compatible with `i18next@22.x.x` and is tested against it.
8 |
9 | Major version of this library can include breaking changes, but it will be soundness.
10 |
--------------------------------------------------------------------------------
/apps/web-api-demo/preferred-languages.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | web-api demo
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/apps/website/docs/protocols/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar: false
3 | ---
4 |
5 | # Protocols
6 |
7 | Protocols are agreements between two or more parties on how to connect and communicate with each other. They are the foundation of the Effector's ecosystem. They allow you to connect different parts of the ecosystem with each other without direct dependencies.
8 |
9 | Some protocols are created by the Effector's committee, and some are provided by the community. The following list contains all known protocols:
10 |
11 |
14 |
15 |
128 |
129 |
130 |
136 | ```
137 |
138 | :::
139 |
--------------------------------------------------------------------------------
/packages/factories/src/factories.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, test, vi } from 'vitest';
2 |
3 | import { createFactory } from './create_factory';
4 | import { invoke } from './invoke';
5 |
6 | describe('factories', () => {
7 | test('invoke calls original creator with passed params', () => {
8 | const mock = vi.fn();
9 | const factory = createFactory((params: number[]) => {
10 | mock(params);
11 |
12 | return params.at(0);
13 | });
14 |
15 | const valueOne = invoke(factory, [1, 2, 3]);
16 | expect(mock).toHaveBeenCalledWith([1, 2, 3]);
17 | expect(valueOne).toBe(1);
18 |
19 | const valueTwo = invoke(factory, [2, 3, 4]);
20 | expect(mock).toHaveBeenCalledWith([2, 3, 4]);
21 | expect(valueTwo).toBe(2);
22 | });
23 |
24 | test('throw error if try to call factory directly', () => {
25 | const myFactory = createFactory((params: number[]) => params.at(0));
26 |
27 | expect(() => myFactory([1, 2, 3])).toThrowErrorMatchingInlineSnapshot(
28 | `[Error: Do not call factory directly, pass it to invoke function instead]`
29 | );
30 | });
31 |
32 | test('throw error if pass function with more than 1 argument', () => {
33 | expect(() =>
34 | // @ts-expect-error It's obvious error, this test for JS-users
35 | createFactory((first: number, second: number) => {
36 | return 1;
37 | })
38 | ).toThrowErrorMatchingInlineSnapshot(
39 | `[Error: createFactory does not support functions with more than 1 argument]`
40 | );
41 | });
42 |
43 | test('throw error if call non-factory function in invoke', () => {
44 | expect(() =>
45 | invoke((params: number[]) => params.at(0), [1, 2, 3])
46 | ).toThrowErrorMatchingInlineSnapshot(
47 | `[Error: Function passed to invoke is not created by createFactory]`
48 | );
49 | });
50 | });
51 |
52 | describe('nested factories', () => {
53 | test('throw error on call un-invoked factory inside invoked factoy', () => {
54 | const internalFactory = createFactory(() => {
55 | return 1;
56 | });
57 |
58 | const externalFactory = createFactory(() => {
59 | const internal = internalFactory();
60 | return internal;
61 | });
62 |
63 | expect(() => invoke(externalFactory)).toThrowErrorMatchingInlineSnapshot(
64 | `[Error: Do not call factory directly, pass it to invoke function instead]`
65 | );
66 | });
67 |
68 | test('do not throw error on call invoked factory inside invoked factoy', () => {
69 | const internalFactory = createFactory(() => {
70 | return 1;
71 | });
72 |
73 | const externalFactory = createFactory(() => {
74 | const internal = invoke(internalFactory);
75 | return internal;
76 | });
77 |
78 | expect(() => invoke(externalFactory)).not.toThrowError();
79 | });
80 |
81 | test('valid many nested factories', () => {
82 | const internalFactory = createFactory(() => {
83 | return 1;
84 | });
85 |
86 | const externalFactory = createFactory(() => {
87 | const internal2 = invoke(internalFactory);
88 | const internal1 = invoke(internalFactory);
89 | return { internal1, internal2 };
90 | });
91 |
92 | expect(() => invoke(externalFactory)).not.toThrowError();
93 | });
94 |
95 | test('invalid many nested factories', () => {
96 | const internalFactory = createFactory(() => {
97 | return 1;
98 | });
99 |
100 | const externalFactory = createFactory(() => {
101 | const internal2 = invoke(internalFactory);
102 | const internal1 = internalFactory();
103 | return { internal1, internal2 };
104 | });
105 |
106 | expect(() => invoke(externalFactory)).toThrowErrorMatchingInlineSnapshot(
107 | `[Error: Do not call factory directly, pass it to invoke function instead]`
108 | );
109 | });
110 |
111 | test('valid very many nested factories', () => {
112 | const veryInternalFactory = createFactory(() => {
113 | return 1;
114 | });
115 |
116 | const internalFactory = createFactory(() => {
117 | return invoke(veryInternalFactory);
118 | });
119 |
120 | const externalFactory = createFactory(() => {
121 | const internal2 = invoke(internalFactory);
122 | const internal1 = invoke(internalFactory);
123 | return { internal1, internal2 };
124 | });
125 |
126 | expect(() => invoke(externalFactory)).not.toThrowError();
127 | });
128 |
129 | test('invalid very many nested factories', () => {
130 | const veryInternalFactory = createFactory(() => {
131 | return 1;
132 | });
133 |
134 | const internalFactory = createFactory(() => {
135 | return veryInternalFactory();
136 | });
137 |
138 | const externalFactory = createFactory(() => {
139 | const internal2 = invoke(internalFactory);
140 | const internal1 = invoke(internalFactory);
141 | return { internal1, internal2 };
142 | });
143 |
144 | expect(() => invoke(externalFactory)).toThrowErrorMatchingInlineSnapshot(
145 | `[Error: Do not call factory directly, pass it to invoke function instead]`
146 | );
147 | });
148 | });
149 |
--------------------------------------------------------------------------------
/apps/website/docs/magazine/no_domains.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: You Don't Need Domains
3 | date: 2024-01-26
4 | ---
5 |
6 | # You Don't Need Domains
7 |
8 | [_Domain_](https://effector.dev/docs/api/effector/domain) in Effector is a namespace for [_Events_](https://effector.dev/en/api/effector/event/), [_Effects_](https://effector.dev/docs/api/effector/effect) and [_Stores_](https://effector.dev/docs/api/effector/store). It could be used for two purposes:
9 |
10 | 1. Semantic grouping of units
11 | 2. Bulk operations on units
12 |
13 | However, in most cases, you do not need [_Domains_](https://effector.dev/docs/api/effector/domain) at all. Let us see why.
14 |
15 | ## Semantic Grouping
16 |
17 | JavaScript does have semantic grouping of entities: it is **modules**. Since you do not have an option not to use modules, you will be using them to group your units anyway. So, why do you need another grouping mechanism?
18 |
19 | ::: code-group
20 |
21 | ```ts [module]
22 | // 👇 all units are already grouped by module
23 | // src/features/counter.ts
24 |
25 | import { createEvent, createStore, sample } from 'effector';
26 |
27 | export const increment = createEvent();
28 | export const decrement = createEvent();
29 |
30 | export const $counter = createStore(0);
31 |
32 | sample({
33 | source: $counter,
34 | clock: increment,
35 | fn: (counter) => counter + 1,
36 | target: $counter,
37 | });
38 |
39 | sample({
40 | source: $counter,
41 | clock: decrement,
42 | fn: (counter) => counter - 1,
43 | target: $counter,
44 | });
45 | ```
46 |
47 | ```ts [module and domain]
48 | // 👇 all units are already grouped by module
49 | // src/features/counter.ts
50 |
51 | import { createDomain, createEvent, createStore, sample } from 'effector';
52 |
53 | // AND by domain, so it is redundant
54 | const counterDomain = createDomain();
55 |
56 | export const increment = createEvent({ domain: counterDomain });
57 | export const decrement = createEvent({ domain: counterDomain });
58 |
59 | export const $counter = createStore(0, { domain: counterDomain });
60 |
61 | sample({
62 | source: $counter,
63 | clock: increment,
64 | fn: (counter) => counter + 1,
65 | target: $counter,
66 | });
67 |
68 | sample({
69 | source: $counter,
70 | clock: decrement,
71 | fn: (counter) => counter - 1,
72 | target: $counter,
73 | });
74 | ```
75 |
76 | :::
77 |
78 | ## Bulk Operations
79 |
80 | But [_Domains_](https://effector.dev/docs/api/effector/domain) are not only about grouping. They also allow you to perform bulk operations on units.
81 |
82 | For example, you can reset values of all [_Stores_](https://effector.dev/docs/api/effector/store) in the [_Domain_](https://effector.dev/docs/api/effector/domain) with the following code:
83 |
84 | ```ts
85 | import { createDomain, createStore, createEvent } from 'effector';
86 |
87 | const domain = createDomain();
88 |
89 | export const someEvent = createEvent({ domain });
90 |
91 | export const $store1 = createStore(0, { domain });
92 | export const $store2 = createStore(0, { domain });
93 | export const $store3 = createStore(0, { domain });
94 |
95 | // 👇 callback will be called on every Store in the Domain
96 | domain.onCreateStore((store) => {
97 | store.reset(someEvent);
98 | });
99 | ```
100 |
101 | This approach has a significant drawback: **it is implicit**. In case of creating a new [_Store_](https://effector.dev/docs/api/effector/store) in the [_Domain_](https://effector.dev/docs/api/effector/domain), you will have to remember that trigger of `someEvent` will reset the new [_Store_](https://effector.dev/docs/api/effector/store) as well. It is really easy to forget about it.
102 |
103 | Things become even worse if you have more than one bulk operations in the [_Domain_](https://effector.dev/docs/api/effector/domain).
104 |
105 | Instead of using [_Domains_](https://effector.dev/docs/api/effector/domain), you can **explicitly** perform bulk operations on units. The previous example can be rewritten as follows:
106 |
107 | ```ts
108 | import { createDomain, createStore, createEvent } from 'effector';
109 |
110 | const domain = createDomain(); // [!code --]
111 |
112 | export const someEvent = createEvent({
113 | domain, // [!code --]
114 | });
115 |
116 | export const $store1 = createStore(0, {
117 | domain, // [!code --]
118 | });
119 | export const $store2 = createStore(0, {
120 | domain, // [!code --]
121 | });
122 | export const $store3 = createStore(0, {
123 | domain, // [!code --]
124 | });
125 |
126 | // 👇 callback will be called on every Store in the Domain
127 | domain.onCreateStore((store) => {
128 | store.reset(someEvent); // [!code --]
129 | });
130 |
131 | // 👇 now it is explicit
132 | resetMany({ stores: [$store1, $store2, $store3], reset: someEvent }); // [!code ++]
133 |
134 | function resetMany({ stores, reset }) {
135 | for (const unit of stores) {
136 | unit.reset(reset);
137 | }
138 | }
139 | ```
140 |
141 | This approach not only more explicit but also less verbose, because you do not need to specify [_Domain_](https://effector.dev/docs/api/effector/domain) for every unit.
142 |
143 | ## Summary
144 |
145 | - **Do not use [_Domains_](https://effector.dev/docs/api/effector/domain)** for semantic grouping - use modules instead
146 | - **Do not use [_Domains_](https://effector.dev/docs/api/effector/domain)** for bulk operations - use explicit functions instead
147 |
--------------------------------------------------------------------------------
/apps/website/scripts/jsdoc.mjs:
--------------------------------------------------------------------------------
1 | import glob from 'glob';
2 | import { readFile, writeFile } from 'node:fs/promises';
3 | import { promisify } from 'node:util';
4 | import { resolve } from 'node:path';
5 | import * as babelParser from '@babel/parser';
6 | import { parse as parseComment } from 'comment-parser';
7 | import { asyncWalk } from 'estree-walker';
8 | import prettier from 'prettier';
9 | import { groupBy } from 'lodash-es';
10 |
11 | const files = await promisify(glob)('../../packages/*/src/**/*.ts', {
12 | absolute: true,
13 | });
14 |
15 | const apis = new Map();
16 |
17 | await Promise.all(
18 | files.map(async (file) => {
19 | const packageName = file.match(/packages\/([^/]+)\//)[1];
20 |
21 | if (!apis.has(packageName)) {
22 | apis.set(packageName, []);
23 | }
24 |
25 | const packageApis = apis.get(packageName);
26 |
27 | const content = await readFile(file, 'utf-8');
28 |
29 | asyncWalk(
30 | babelParser.parse(content, {
31 | sourceType: 'module',
32 | plugins: ['typescript', 'jsx', 'estree', 'decorators-legacy'],
33 | }),
34 | {
35 | async enter(node) {
36 | if (node.type !== 'ExportNamedDeclaration') {
37 | return;
38 | }
39 |
40 | let kind = '';
41 | let name = '';
42 | switch (node.declaration?.type) {
43 | case 'TSTypeAliasDeclaration':
44 | name = node.declaration.id.name;
45 | kind = 'type';
46 | break;
47 | case 'FunctionDeclaration':
48 | name = node.declaration.id.name;
49 | kind = 'function';
50 | break;
51 | case 'TSDeclareFunction':
52 | name = node.declaration.id.name;
53 | kind = 'function';
54 | break;
55 | case 'VariableDeclaration':
56 | name = node.declaration.declarations[0].id.name;
57 | kind = 'variable';
58 | break;
59 | }
60 |
61 | const comments =
62 | node.leadingComments?.filter(
63 | (comment) => comment.type === 'CommentBlock'
64 | ) ?? [];
65 |
66 | if (!name || !kind || comments?.length === 0) {
67 | return;
68 | }
69 |
70 | const parsedDocs = comments
71 | .filter((comment) => comment.value.startsWith('*'))
72 | .flatMap((comment) =>
73 | // comment-parser requires /* */ around the comment
74 | parseComment('/*' + comment.value + '*/')
75 | );
76 |
77 | for (const doc of parsedDocs) {
78 | const privateTag = doc.tags.find((tag) => tag.tag === 'private');
79 |
80 | if (privateTag) {
81 | continue;
82 | }
83 |
84 | const exampleTags = doc.tags.filter((tag) => tag.tag === 'example');
85 |
86 | let examples = await Promise.all(
87 | exampleTags.map((tag) =>
88 | prettier.format(tag.description, { parser: 'babel' })
89 | )
90 | );
91 |
92 | const overloadTag = doc.tags.find((tag) => tag.tag === 'overload');
93 |
94 | const sinceTag = doc.tags.find((tag) => tag.tag === 'since');
95 |
96 | packageApis.push({
97 | kind,
98 | name,
99 | description: doc.description,
100 | examples,
101 | alias: overloadTag?.name,
102 | since: sinceTag?.name,
103 | });
104 | }
105 | },
106 | }
107 | );
108 | })
109 | );
110 |
111 | for (const [packageName, packageApis] of apis) {
112 | if (packageApis.length === 0) {
113 | continue;
114 | }
115 |
116 | const groupedApis = groupBy(packageApis, (api) => api.name);
117 |
118 | const filePath = resolve('docs', packageName, 'api.md');
119 |
120 | const content = ['# APIs', 'Full list of available APIs.'];
121 |
122 | for (const [name, overloads] of Object.entries(groupedApis)) {
123 | const tsOnly = overloads.every((api) => api.kind === 'type');
124 | const sinceAll = overloads.every((api) => api.since);
125 |
126 | content.push(
127 | `## \`${name}\` ${[
128 | tsOnly && '',
129 | sinceAll && ``,
130 | ]
131 | .filter(Boolean)
132 | .join('')}`
133 | );
134 |
135 | if (overloads.length === 1) {
136 | const [onlyOverload] = overloads;
137 | content.push(onlyOverload.description);
138 | content.push(
139 | ...onlyOverload.examples.map((example) => '```ts\n' + example + '\n```')
140 | );
141 | } else {
142 | content.push('Is has multiple overloads 👇');
143 | for (const overload of overloads) {
144 | content.push(
145 | `### \`${overload.alias ?? overload.name}\` ${[
146 | !sinceAll &&
147 | overload.since &&
148 | ``,
149 | ].join(' ')}`
150 | );
151 | content.push(overload.description);
152 | content.push(
153 | ...overload.examples.map((example) => '```ts\n' + example + '\n```')
154 | );
155 | }
156 | }
157 | }
158 |
159 | await writeFile(filePath, content.join('\n\n'));
160 | }
161 |
--------------------------------------------------------------------------------
/apps/website/docs/i18next/index.md:
--------------------------------------------------------------------------------
1 | # i18next
2 |
3 | A powerful internationalization framework for Effector which is based on [i18next](https://www.i18next.com/).
4 |
5 | ## Installation
6 |
7 | First, you need to install integration and its peer dependency:
8 |
9 | ::: code-group
10 |
11 | ```sh [pnpm]
12 | pnpm install @withease/i18next i18next
13 | ```
14 |
15 | ```sh [yarn]
16 | yarn add @withease/i18next i18next
17 | ```
18 |
19 | ```sh [npm]
20 | npm install @withease/i18next i18next
21 | ```
22 |
23 | :::
24 |
25 | ## Initialization
26 |
27 | All you need to do is to create an integration by calling `createI18nextIntegration` with an integration options:
28 |
29 | - `instance`: an instance of i18next in various forms.
30 | - `setup`: after this [_Event_](https://effector.dev/en/api/effector/event/) all listeners will be installed, and the integration will be ready to use; it is required because it is better to use [explicit initialization _Event_ in the application](/magazine/explicit_start).
31 | - `teardown?`: after this [_Event_](https://effector.dev/en/api/effector/event/) all listeners will be removed, and the integration will be ready to be destroyed.
32 |
33 | ### Use replaceable static `i18next` instance
34 |
35 | In the simplest case, you can pass an i18next instance to the integration.
36 |
37 | ```ts{9-11}
38 | import i18next from 'i18next';
39 | import { createStore, createEvent, fork, allSettled } from 'effector';
40 | import { createI18nextIntegration } from '@withease/i18next';
41 |
42 | // Event that should be called after application initialization
43 | const appStarted = createEvent();
44 |
45 | // Create Store for i18next instance
46 | const $i18nextInstance = createStore(i18next.createInstance(/* ... */), {
47 | serialize: 'ignore',
48 | });
49 |
50 | const integration = createI18nextIntegration({
51 | // Pass Store with i18next instance to the integration
52 | instance: $i18nextInstance,
53 | setup: appStarted,
54 | });
55 |
56 | // You can replace $someInstance later during runtime
57 | // e.g., during fork on client or server
58 | ```
59 |
60 | ### Use replaceable asynchronous `i18next` instance
61 |
62 | Sometimes you need to create an instance asynchronously. In this case, you can pass an [_Effect_](https://effector.dev/docs/api/effector/effect) that creates an instance.
63 |
64 | ```ts{9-11}
65 | import i18next from 'i18next';
66 | import { createStore, createEvent, fork, allSettled } from 'effector';
67 | import { createI18nextIntegration } from '@withease/i18next';
68 |
69 | // Event that should be called after application initialization
70 | const appStarted = createEvent();
71 |
72 | // Create Effect that creates i18next instance
73 | const createI18nextFx = createEffect(() =>
74 | i18next.use(/* ... */).init(/* ... */)
75 | );
76 |
77 | const integration = createI18nextIntegration({
78 | // Pass Effect that creates i18next instance to the integration
79 | instance: createI18nextFx,
80 | setup: appStarted,
81 | });
82 |
83 | // You can replace createI18nextFx later during runtime
84 | // e.g., during fork on client or server
85 | ```
86 |
87 | ### Use static `i18next` instance
88 |
89 | Even though it is better to use a replaceable instance to [avoid global state](/magazine/global_variables) and make it possible to replace the instance during runtime, you can pass a static instance as well.
90 |
91 | ```ts{9}
92 | import i18next from 'i18next';
93 | import { createStore, createEvent } from 'effector';
94 | import { createI18nextIntegration } from '@withease/i18next';
95 |
96 | // Event that should be called after application initialization
97 | const appStarted = createEvent();
98 |
99 | const integration = createI18nextIntegration({
100 | instance: i18next.createInstance(/* ... */),
101 | setup: appStarted,
102 | });
103 | ```
104 |
105 | ### Use static asynchronous `i18next` instance
106 |
107 | The same approach can be used with an asynchronous instance.
108 |
109 | ```ts{9}
110 | import i18next from 'i18next';
111 | import { createStore, createEvent } from 'effector';
112 | import { createI18nextIntegration } from '@withease/i18next';
113 |
114 | // Event that should be called after application initialization
115 | const appStarted = createEvent();
116 |
117 | const integration = createI18nextIntegration({
118 | instance: () => i18next.use(/* ... */).init(/* ... */),
119 | setup: appStarted,
120 | });
121 | ```
122 |
123 | ## Usage
124 |
125 | Returned from `createI18nextIntegration` integration contains the following fields:
126 |
127 | - [`$t`](/i18next/t) is a [_Store_](https://effector.dev/docs/api/effector/store) containing a [translation function](https://www.i18next.com/overview/api#t)
128 | - [`translated`](/i18next/translated) which can be used as a shorthand for `$t`
129 | - [`$isReady`](/i18next/is_ready) is a [_Store_](https://effector.dev/docs/api/effector/store) containing a boolean value that indicates whether the integration is ready to use
130 | - [`reporting`](/i18next/reporting) is an object with the fields that allow you to track different events of the integration
131 | - [`$language`](/i18next/language) is a [_Store_](https://effector.dev/docs/api/effector/store) containing the current language
132 | - [`changeLanguageFx`](/i18next/change_language) is an [_Effect_](https://effector.dev/docs/api/effector/effect) that changes the current language
133 | - [`$instance`](/i18next/instance) is a [_Store_](https://effector.dev/docs/api/effector/store) containing the instance of i18next that is used by the integration
134 |
--------------------------------------------------------------------------------
/apps/website/docs/magazine/explicit_start.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Explicit start of the app
3 | date: 2024-01-26
4 | ---
5 |
6 | # Explicit start of the app
7 |
8 | In Effector [_Events_](https://effector.dev/en/api/effector/event/) can not be triggered implicitly. It gives you more control over the app's lifecycle and helps to avoid unexpected behavior.
9 |
10 | ## The code
11 |
12 | In the simplest case, you can just create something like `appStarted` [_Event_](https://effector.dev/en/api/effector/event/) and trigger it right after the app initialization. Let us pass through the code line by line and explain what's going on here.
13 |
14 | 1. Create start [_Event_](https://effector.dev/en/api/effector/event/)
15 |
16 | This [_Event_](https://effector.dev/en/api/effector/event/) will be used to trigger the start of the app. For example, you can attach some global listeners after this it.
17 |
18 | ```ts{3}
19 | import { createEvent, fork, allSettled } from "effector";
20 |
21 | const appStarted = createEvent();
22 |
23 | const scope = fork();
24 |
25 | await allSettled(appStarted, { scope });
26 | ```
27 |
28 | 2. Create isolated [_Scope_](https://effector.dev/docs/api/effector/scope)
29 |
30 | Fork API allows you to create isolated [_Scope_](https://effector.dev/docs/api/effector/scope) which will be used across the app. It helps you to [prevent using global state](/magazine/global_variables) and avoid unexpected behavior.
31 |
32 | ```ts{5}
33 | import { createEvent, fork, allSettled } from "effector";
34 |
35 | const appStarted = createEvent();
36 |
37 | const scope = fork();
38 |
39 | await allSettled(appStarted, { scope });
40 | ```
41 |
42 | 3. Trigger start [_Event_](https://effector.dev/en/api/effector/event/) on the patricular [_Scope_](https://effector.dev/docs/api/effector/scope)
43 |
44 | [`allSettled`](https://effector.dev/docs/api/effector/allSettled) function allows you to start an [_Event_](https://effector.dev/en/api/effector/event/) on particular [_Scope_](https://effector.dev/docs/api/effector/scope) and wait until all computations will be finished.
45 |
46 | ```ts{7}
47 | import { createEvent, fork, allSettled } from "effector";
48 |
49 | const appStarted = createEvent();
50 |
51 | const scope = fork();
52 |
53 | await allSettled(appStarted, { scope });
54 | ```
55 |
56 | ## The reasons
57 |
58 | The main reason for this approach is it allows you to control the app's lifecycle. It helps you to avoid unexpected behavior and make your app more predictable in some cases. Let us say we have a module with the following code:
59 |
60 | ```ts
61 | // app.ts
62 | import { createStore, createEvent, sample, scopeBind } from 'effector';
63 |
64 | const $counter = createStore(0);
65 | const increment = createEvent();
66 |
67 | const startIncrementationIntervalFx = createEffect(() => {
68 | const boundIncrement = scopeBind(increment, { safe: true });
69 |
70 | setInterval(() => {
71 | boundIncrement();
72 | }, 1000);
73 | });
74 |
75 | sample({
76 | clock: increment,
77 | source: $counter,
78 | fn: (counter) => counter + 1,
79 | target: $counter,
80 | });
81 |
82 | startIncrementationIntervalFx();
83 | ```
84 |
85 | ### Tests
86 |
87 | We believe that any serious application has to be testable, so we have to isolate application lifecycle inside particular test-case. In case of implicit start (start of model logic by module execution), it will be impossible to test the app's behavior in different states.
88 |
89 | ::: tip
90 | [`scopeBind`](https://effector.dev/docs/api/effector/scopeBind) function allows you to bind an [_Event_](https://effector.dev/en/api/effector/event/) to particular [_Scope_](https://effector.dev/docs/api/effector/scope), more details you can find in the article [about Fork API rules](/magazine/fork_api_rules).
91 | :::
92 |
93 | Now, to test the app's behavior, we have to mock `setInterval` function and check that `$counter` value is correct after particular time.
94 |
95 | ```ts
96 | // app.test.ts
97 | import { $counter } from './app';
98 |
99 | test('$counter should be 5 after 5 seconds', async () => {
100 | // ... test
101 | });
102 |
103 | test('$counter should be 10 after 10 seconds', async () => {
104 | // ... test
105 | });
106 | ```
107 |
108 | But, counter will be started immediately after the module execution, and we will not be able to test the app's behavior in different states.
109 |
110 | ### SSR
111 |
112 | In case of SSR, we have to start all application's logic on every user's request, and it will be impossible to do with implicit start.
113 |
114 | ```ts
115 | // server.ts
116 | import * as app from './app';
117 |
118 | function handleRequest(req, res) {
119 | // ...
120 | }
121 | ```
122 |
123 | But, counter will be started immediately after the module execution (aka application initialization), and we will not be able to start the app's logic on every user's request.
124 |
125 | ### Add explicit start
126 |
127 | Let us rewrite the code and add explicit start of the app.
128 |
129 | ```ts
130 | // app.ts
131 | import { createStore, createEvent, sample, scopeBind } from 'effector';
132 |
133 | const $counter = createStore(0);
134 | const increment = createEvent();
135 |
136 | const startIncrementationIntervalFx = createEffect(() => {
137 | const boundIncrement = scopeBind(increment, { safe: true });
138 |
139 | setInterval(() => {
140 | boundIncrement();
141 | }, 1000);
142 | });
143 |
144 | sample({
145 | clock: increment,
146 | source: $counter,
147 | fn: (counter) => counter + 1,
148 | target: $counter,
149 | });
150 |
151 | startIncrementationIntervalFx(); // [!code --]
152 | const appStarted = createEvent(); // [!code ++]
153 | sample({ clock: appStarted, target: startIncrementationIntervalFx }); // [!code ++]
154 | ```
155 |
156 | That is it! Now we can test the app's behavior in different states and start the app's logic on every user's request.
157 |
158 | :::tip
159 | In real-world applications, it is better to add not only explicit start of the app, but also explicit stop of the app. It will help you to avoid memory leaks and unexpected behavior.
160 | :::
161 |
162 | ## One more thing
163 |
164 | In this recipe, we used application-wide `appStarted` [_Event_](https://effector.dev/en/api/effector/event/) to trigger the start of the app. However, in real-world applications, it is better to use more granular [_Events_](https://effector.dev/en/api/effector/event/) to trigger the start of the particular part of the app.
165 |
166 | ## Recap
167 |
168 | - Do not execute any logic just on module execution
169 | - Use explicit start [_Event_](https://effector.dev/en/api/effector/event/) of the application
170 |
--------------------------------------------------------------------------------
/apps/website/docs/contracts/index.md:
--------------------------------------------------------------------------------
1 |
15 |
16 | # contracts
17 |
18 | Extremely small library (less than **{{maxSize}}** controlled by CI) for creating [_Contracts_](/protocols/contract) that allows you to introduce data validation on edges of the application with no performance compromises.
19 |
20 | ## Installation
21 |
22 | First, you need to install package:
23 |
24 | ::: code-group
25 |
26 | ```sh [pnpm]
27 | pnpm install @withease/contracts
28 | ```
29 |
30 | ```sh [yarn]
31 | yarn add @withease/contracts
32 | ```
33 |
34 | ```sh [npm]
35 | npm install @withease/contracts
36 | ```
37 |
38 | :::
39 |
40 | ## Creating a _Contract_
41 |
42 | `@withease/contracts` exports bunch of utilities that can be used to create a _Contract_, read the full API reference [here](/contracts/api). Any of the utilities returns a _Contract_ object, that accepts something `unknown` and checks if it is something concrete defined by the used utility.
43 |
44 |
45 |
46 | ## Extracting types from a _Contract_
47 |
48 | `@withease/contracts` provides a special type `UnContract` that can be used to extract a type from a _Contract_.
49 |
50 | ```ts
51 | import { type UnContract, obj, str, num } from '@withease/contracts';
52 |
53 | const UserContract = obj({
54 | id: num,
55 | name: str,
56 | email: str,
57 | });
58 |
59 | // type User = { id: number, name: string, email: string }
60 | type User = UnContract;
61 | ```
62 |
63 | ## Usage of a _Contract_
64 |
65 | `@withease/contracts` is designed to be compatible with Effector's ecosystem without additional interop, so most of the time you can pass created [_Contract_](/protocols/contract) to other Effector's libraries as is.
66 |
67 | ### Farfetched
68 |
69 | [Farfetched](https://ff.effector.dev) is the advanced data fetching tool for web applications based of Effector. It suggests to ensure that data received from the server is conforms desired [_Contract_](/protocols/contract).
70 |
71 | ```ts
72 | import { createJsonQuery } from '@farfetched/core';
73 | import { obj, str, arr, val, or } from '@withease/contracts';
74 |
75 | const characterQuery = createJsonQuery({
76 | params: declareParams<{ id: number }>(),
77 | request: {
78 | method: 'GET',
79 | url: ({ id }) => `https://rickandmortyapi.com/api/character/${id}`,
80 | },
81 | response: {
82 | // after receiving data from the server
83 | // check if it is conforms the Contract to ensure
84 | // API does not return something unexpected
85 | contract: obj({
86 | id: str,
87 | name: str,
88 | status: Status,
89 | species: str,
90 | type: str,
91 | gender: Gender,
92 | origin: obj({ name: str, url: str }),
93 | location: obj({ name: str, url: str }),
94 | image: or(val('Female'), val('Male'), val('Genderless')),
95 | episode: arr(str),
96 | }),
97 | },
98 | });
99 | ```
100 |
101 | ### effector-storage
102 |
103 | [`effector-storage`](https://github.com/yumauri/effector-storage) is a small module for Effector to sync stores with different storages (local storage, session storage, async storage, IndexedDB, cookies, server side storage, etc).
104 |
105 | Since data is stored in an external storage it is important to validate it before using it in the application.
106 |
107 | ```ts
108 | import { createStore } from 'effector';
109 | import { persist } from 'effector-storage';
110 | import { num } from '@withease/contracts';
111 |
112 | const $counter = createStore(0);
113 |
114 | persist({
115 | store: $counter,
116 | key: 'counter',
117 | // after reading value from a storage check if a value is number
118 | // to avoid pushing invalid data to the Store
119 | contract: num,
120 | });
121 | ```
122 |
123 | ## Integration with other libraries
124 |
125 | Since `@withease/contracts` is compatible [_Contract_](/protocols/contract) protocol it can be used with any library that supports it.
126 |
127 | For instance, you can define a part of a [_Contract_](/protocols/contract) with [Zod](https://zod.dev/) and combine it with `@withease/contracts`:
128 |
129 | ```ts
130 | import { z } from 'zod';
131 | import { arr, obj } from '@withease/contracts';
132 | import { zodContract } from '@farfetched/zod';
133 |
134 | const User = z.object({
135 | name: z.string(),
136 | });
137 |
138 | const MyContract = arr(
139 | obj({
140 | // 👇 easily integrate Zod via compatibility layer
141 | users: zodContract(User),
142 | })
143 | );
144 | ```
145 |
146 | The full list of libraries that support _Contract_ protocol can be found [here](/protocols/contract).
147 |
148 | ## Differences from other libraries
149 |
150 |
151 | It is extremely small and we mean it 👇
152 |
153 |
154 |
155 |
156 |
157 |
158 | ::: tip
159 | Data fetched directly from https://esm.run/ and updates on every commit.
160 | :::
161 |
162 |
163 |
164 | It is significantly smaller than other libraries for creating _Contracts_.
165 |
166 |
167 | Of course smaller size is comes with some trade-offs, but we believe that in most cases it is worth it. `@withease/contracts` covers most of the common cases but does not try to be a silver bullet for all possible cases. It does not aim to have the following features from other libraries:
168 |
169 | - Branded types ([like in Runtypes](https://github.com/runtypes/runtypes?tab=readme-ov-file#branded-types))
170 | - Advanced string-validators ([like IP-validation in Zod](https://zod.dev/?id=ip-addresses))
171 | - Promise schemas ([like in Zod](https://zod.dev/?id=promise))
172 | - Error i18n ([like in Valibot](https://valibot.dev/guides/internationalization/))
173 | - ...and many other features that are not needed in _most_ of the cases
174 |
175 | ::: tip Q: What if I started a project with `@withease/contracts` and then realized that I need some of the features that are not covered by it?
176 | A: No worries! You can easily integrate `@withease/contracts` with other libraries that have the features you need. Check out the [Integration with other libraries](#integration-with-other-libraries) section for more details.
177 | :::
178 |
--------------------------------------------------------------------------------
/apps/website/docs/redux/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | outline: [2, 3]
3 | ---
4 |
5 | # @withease/redux
6 |
7 | Minimalistic package to allow simpler migration from Redux to Effector.
8 | Also, can handle any other use case, where one needs to communicate with Redux Store from Effector's code.
9 |
10 | :::info
11 | This is an API reference article, for the Redux -> Effector migration guide [see the "Migrating from Redux to Effector" article](/magazine/migration_from_redux).
12 | :::
13 |
14 | ## Installation
15 |
16 | First, you need to install package:
17 |
18 | ::: code-group
19 |
20 | ```sh [pnpm]
21 | pnpm install @withease/redux
22 | ```
23 |
24 | ```sh [yarn]
25 | yarn add @withease/redux
26 | ```
27 |
28 | ```sh [npm]
29 | npm install @withease/redux
30 | ```
31 |
32 | :::
33 |
34 | ## API
35 |
36 | ### `createReduxIntegration`
37 |
38 | Effector <-> Redux interoperability works through special "interop" object, which provides Effector-compatible API to Redux Store.
39 |
40 | ```ts
41 | const myReduxStore = configureStore({
42 | // ...
43 | });
44 |
45 | const reduxInterop = createReduxIntegration({
46 | reduxStore: myReduxStore,
47 | setup: appStarted,
48 | });
49 | ```
50 |
51 | Explicit `setup` event is required to initialize the interoperability. Usually it would be an `appStarted` event or any other "app's lifecycle" event.
52 |
53 | You can read more about this practice [in the "Explicit start of the app" article](/magazine/explicit_start).
54 |
55 | #### Async setup
56 |
57 | You can also defer interop object initialization and Redux Store creation.
58 |
59 | The `createReduxIntegration` overload without explicit `reduxStore` allows you to pass the Store via `setup` event later.
60 |
61 | ```ts
62 | // src/shared/redux-interop
63 | export const startReduxInterop = createEvent();
64 | export const reduxInterop = createReduxIntegration({
65 | setup: startReduxInterop,
66 | });
67 |
68 | // src/entrypoint.ts
69 | import { startReduxInterop } from 'shared/redux-interop';
70 |
71 | const myReduxStore = configureStore({
72 | // ...
73 | });
74 |
75 | startReduxInterop(myReduxStore);
76 | // or, if you use the Fork API
77 | allSettled(startReduxInterop, {
78 | scope: clientScope,
79 | params: myReduxStore,
80 | });
81 | ```
82 |
83 | In that case the type support for `reduxInterop.$state` will be slightly worse and `reduxInterop.dispatch` will be no-op (and will show warnings in console) until interop object is provided with Redux Store.
84 |
85 | ☝️ This is useful, if your project has cyclic dependencies.
86 |
87 | ### Interoperability object
88 |
89 | Redux Interoperability object provides few useful APIs.
90 |
91 | #### `reduxInterop.$state`
92 |
93 | This is an Effector's Store, which contains **the state** of the provided instance of Redux Store.
94 |
95 | It is useful, as it allows to represent any part of Redux state as an Effector store.
96 |
97 | ```ts
98 | import { combine } from 'effector';
99 |
100 | const $user = combine(reduxInterop.$state, (x) => x.user);
101 | ```
102 |
103 | :::tip
104 | Notice, that `reduxInterop.$state` store will use Redux Store typings, if those are provided. So it is recommended to properly type your Redux Store.
105 | :::
106 |
107 | #### `reduxInterop.dispatch`
108 |
109 | This is an Effector's Effect, which calls Redux Store's `dispatch` method under the hood.
110 | Since it is a normal [Effect](https://effector.dev/en/api/effector/effect) - it supports all methods of `Effect` type.
111 |
112 | :::tip
113 | It is recommended to create separate events for each specific action via `.prepend` method of `Effect`.
114 | :::
115 |
116 | ```ts
117 | const updateUserName = reduxInterop.dispatch.prepend((name: string) =>
118 | userSlice.changeName(name)
119 | );
120 |
121 | sample({
122 | clock: saveButtonClicked,
123 | source: $nextName,
124 | target: updateUserName,
125 | });
126 | ```
127 |
128 | It is also possible to convert a Redux Thunk to `Effect` by using Effector's [`attach` operator](https://effector.dev/en/api/effector/attach/).
129 |
130 | ```ts
131 | import { createAsyncThunk } from '@reduxjs/toolkit';
132 | import { attach } from 'effector';
133 |
134 | const someThunk = createAsyncThunk(
135 | 'some/thunk',
136 | async (p: number, { dispatch }) => {
137 | await new Promise((resolve) => setTimeout(resolve, p));
138 |
139 | return dispatch(someSlice.actions.doSomething());
140 | }
141 | );
142 |
143 | /**
144 | * This is a redux-thunk, converted into an effector Effect.
145 | *
146 | * This allows gradual migration from redux-thunks to effector Effects
147 | */
148 | const someThunkFx = attach({
149 | mapParams: (p: number) => someThunk(p),
150 | effect: interop.dispatch,
151 | });
152 |
153 | const promise = someThunkFx(42);
154 | // ☝️ `someThunk` will be dispatched under the hood
155 | // `someThunkFx` will return an Promise, which will be resolved once someThunk is resolved
156 | ```
157 |
158 | #### `reduxInterop.$reduxStore`
159 |
160 | This is an Effector's Store, which contains provided instance of Redux Store.
161 |
162 | It is useful, since it makes possible to use [Effector's Fork API to write tests](https://effector.dev/en/guides/testing/) for the logic, contained in the Redux Store!
163 |
164 | So even if the logic is mixed between the two like this:
165 |
166 | ```ts
167 | // app code
168 | const myReduxStore = configureStore({
169 | // ...
170 | });
171 |
172 | const reduxInterop = createReduxIntegration({
173 | reduxStore: myReduxStore,
174 | setup: appStarted,
175 | });
176 |
177 | // user model
178 | const $user = combine(reduxInterop.$state, (x) => x.user);
179 |
180 | const updateUserName = reduxInterop.dispatch.prepend((name: string) =>
181 | userSlice.changeName(name)
182 | );
183 |
184 | sample({
185 | clock: saveButtonClicked,
186 | source: $nextName,
187 | target: updateUserName,
188 | });
189 | ```
190 |
191 | It is still possible to write a proper test like this:
192 |
193 | ```ts
194 | test('username updated after save button click', async () => {
195 | const mockStore = configureStore({
196 | // ...
197 | });
198 |
199 | const scope = fork({
200 | values: [
201 | // Providing mock version of the redux store
202 | [reduxInterop.$reduxStore, mockStore],
203 | // Mocking anything else, if needed
204 | [$nextName, 'updated'],
205 | ],
206 | });
207 |
208 | await allSettled(appStarted, { scope });
209 |
210 | expect(scope.getState($userName)).toBe('initial');
211 |
212 | await allSettled(saveButtonClicked, { scope });
213 |
214 | expect(scope.getState($userName)).toBe('updated');
215 | });
216 | ```
217 |
218 | ☝️ This test will be especially useful in the future, when this part of logic will be ported to Effector.
219 |
220 | :::tip
221 | Notice, that it is recommended to create a mock version of Redux Store for any tests like this, since the Store contains state, which could leak between the tests.
222 | :::
223 |
--------------------------------------------------------------------------------
/packages/redux/src/integration.ts:
--------------------------------------------------------------------------------
1 | import type { Unit, StoreWritable, Store, Effect } from 'effector';
2 | import type { Store as ReduxStore, Action } from 'redux';
3 | import {
4 | createStore,
5 | createEvent,
6 | is,
7 | sample,
8 | attach,
9 | scopeBind,
10 | } from 'effector';
11 |
12 | /**
13 | * Type for any thunk-like thing, which can be dispatched to Redux store
14 | *
15 | * Since generally Thunk is a any function, we can't type it properly
16 | */
17 | type AnyThunkLikeThing = (...args: any[]) => any;
18 |
19 | /**
20 | *
21 | * Utility function to create an Effector API to interact with Redux store,
22 | * useful for cases like soft migration from Redux to Effector.
23 | *
24 | * If `reduxStore` is not provided in initial config, then it must be provided via `setup` event call.
25 | * If Redux Store is not provided, then it will be set to `null` initially and `reduxInterop.dispatch` will complain about it.
26 | *
27 | * @param config - interop config
28 | * @param config.reduxStore - (optional) initial redux store instance
29 | * @param config.setup - effector unit which will setup subscription to the store
30 | * @returns Interop API object
31 | */
32 | export function createReduxIntegration<
33 | State = unknown,
34 | Act extends Action = { type: string; [k: string]: unknown },
35 | Ext extends {} = {}
36 | >(config: {
37 | setup: Unit>;
38 | }): {
39 | /**
40 | * Effector store containing the Redux store
41 | *
42 | * You can use it to substitute Redux store instance, while writing tests via Effector's Fork API
43 | * @example
44 | * ```
45 | * const scope = fork({
46 | * values: [
47 | * [reduxInterop.$reduxStore, reduxStoreMock]
48 | * ]
49 | * })
50 | * ```
51 | */
52 | $reduxStore: StoreWritable>;
53 | /**
54 | * Effector's event, which will trigger Redux store dispatch
55 | *
56 | * @example
57 | * ```
58 | * const updateName = reduxInterop.dispatch.prepend((name: string) => updateNameAction(name));
59 | * ```
60 | */
61 | dispatch: Effect;
62 | /**
63 | * Effector store containing the state of the Redux store
64 | *
65 | * You can use it to subscribe to the Redux store state changes in Effector
66 | * @example
67 | * ```
68 | * const $userName = combine(reduxInterop.$state, state => state.user.name)
69 | * ```
70 | */
71 | $state: Store;
72 | };
73 | /**
74 | *
75 | * Utility function to create an Effector API to interact with Redux store,
76 | * useful for cases like soft migration from Redux to Effector.
77 | *
78 | * @param config - interop config
79 | * @param config.reduxStore - a redux store
80 | * @param config.setup - effector unit which will setup subscription to the store
81 | * @returns Interop API object
82 | */
83 | export function createReduxIntegration<
84 | State = unknown,
85 | Act extends Action = { type: string; [k: string]: unknown },
86 | Ext extends {} = {}
87 | >(config: {
88 | reduxStore: ReduxStore;
89 | setup: Unit;
90 | }): {
91 | /**
92 | * Effector store containing the Redux store
93 | *
94 | * You can use it to substitute Redux store instance, while writing tests via Effector's Fork API
95 | * @example
96 | * ```
97 | * const scope = fork({
98 | * values: [
99 | * [reduxInterop.$reduxStore, reduxStoreMock]
100 | * ]
101 | * })
102 | * ```
103 | */
104 | $reduxStore: StoreWritable>;
105 | /**
106 | * Effector's event, which will trigger Redux store dispatch
107 | *
108 | * @example
109 | * ```
110 | * const updateName = reduxInterop.dispatch.prepend((name: string) => updateNameAction(name));
111 | * ```
112 | */
113 | dispatch: Effect;
114 | /**
115 | * Effector store containing the state of the Redux store
116 | *
117 | * You can use it to subscribe to the Redux store state changes in Effector
118 | * @example
119 | * ```
120 | * const $userName = combine(reduxInterop.$state, state => state.user.name)
121 | * ```
122 | */
123 | $state: Store;
124 | };
125 | export function createReduxIntegration<
126 | State = unknown,
127 | Act extends Action = { type: string; [k: string]: unknown },
128 | Ext extends {} = {}
129 | // Implementation type is `(any) => any`, so TS doesn't complain about overloads being "incompatible"
130 | // We do that, because they are incompatible, but that is for a reason - those are intended for different use-cases
131 | // e.g. explicit `reduxStore` overload can just infer the types right away + expect that the store and the state are always available
132 | >(config: any): any {
133 | const { reduxStore, setup } = config;
134 | if (!is.unit(setup)) {
135 | throw new Error('setup must be an effector unit');
136 | }
137 |
138 | if (reduxStore) {
139 | /**
140 | * Only assert `reduxStore`, if it was provided explicitly
141 | */
142 | assertReduxStore(reduxStore);
143 | }
144 |
145 | const $reduxStore = createStore(reduxStore ?? null, {
146 | serialize: 'ignore',
147 | name: 'redux/$reduxStore',
148 | });
149 |
150 | if (!reduxStore) {
151 | /**
152 | * If no `reduxStore` was provided in the initial config,
153 | * then this is an async setup case
154 | *
155 | * So `reduxStore` will be provided by the `setup` event
156 | */
157 | sample({
158 | clock: setup,
159 | target: $reduxStore,
160 | });
161 | }
162 |
163 | const stateUpdated = createEvent();
164 |
165 | const $state = createStore(reduxStore?.getState() ?? null, {
166 | serialize: 'ignore',
167 | name: 'redux/$state',
168 | skipVoid: false,
169 | }).on(stateUpdated, (_, state) => state);
170 |
171 | const dispatchFx = attach({
172 | source: $reduxStore,
173 | effect(store, action: Act | AnyThunkLikeThing) {
174 | assertReduxStore(store);
175 |
176 | return store.dispatch(action as Act) as unknown;
177 | },
178 | });
179 |
180 | const reduxInteropSetupFx = attach({
181 | source: $reduxStore,
182 | effect(store) {
183 | assertReduxStore(store);
184 |
185 | const sendUpdate = scopeBind(stateUpdated, { safe: true });
186 |
187 | sendUpdate(store.getState());
188 |
189 | store.subscribe(() => {
190 | sendUpdate(store.getState());
191 | });
192 | },
193 | });
194 |
195 | sample({
196 | clock: setup,
197 | target: reduxInteropSetupFx,
198 | });
199 |
200 | /**
201 | * Logging any errors from the interop to the console for simplicity
202 | */
203 | sample({
204 | clock: [dispatchFx.failData, reduxInteropSetupFx.failData],
205 | }).watch(console.error);
206 |
207 | return {
208 | $reduxStore,
209 | dispatch: dispatchFx,
210 | $state,
211 | };
212 | }
213 |
214 | function assertReduxStore(reduxStore: any): asserts reduxStore is ReduxStore {
215 | if (
216 | !reduxStore ||
217 | !reduxStore.dispatch ||
218 | !reduxStore.getState ||
219 | !reduxStore.subscribe
220 | ) {
221 | throw new Error('reduxStore must be provided and should be a Redux store');
222 | }
223 | }
224 |
--------------------------------------------------------------------------------