Skip to content

Commit b7aef67

Browse files
authored
feat(reflect/hooks): pass component props to mounted hook (#89)
* feat(reflect/hooks): pass component props to `mounted` hook Closes #88 * test(type-tests/reflect): should error if mounted event doesn't satisfy component props
1 parent 4c8b159 commit b7aef67

File tree

8 files changed

+149
-32
lines changed

8 files changed

+149
-32
lines changed

Readme.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ To learn more, please read the [full Motivation article](https://reflect.effecto
6666
## Release process
6767

6868
1. Check out the [draft release](https://github.com/effector/reflect/releases).
69-
1. All PRs should have correct labels and useful titles. You can [review available labels here](https://github.com/effector/reflect/blob/master/.github/release-drafter.yml).
70-
1. Update labels for PRs and titles, next [manually run the release drafter action](https://github.com/effector/reflect/actions/workflows/release-drafter.yml) to regenerate the draft release.
71-
1. Review the new version and press "Publish"
72-
1. If required check "Create discussion for this release"
69+
2. All PRs should have correct labels and useful titles. You can [review available labels here](https://github.com/effector/reflect/blob/master/.github/release-drafter.yml).
70+
3. Update labels for PRs and titles, next [manually run the release drafter action](https://github.com/effector/reflect/actions/workflows/release-drafter.yml) to regenerate the draft release.
71+
4. Review the new version and press "Publish"
72+
5. If required check "Create discussion for this release"

public-types/reflect.d.ts

+10-10
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ type UseUnitConfig = Parameters<typeof useUnit>[1];
77

88
type UnbindableProps = 'key' | 'ref';
99

10-
type Hooks = {
11-
mounted?: EventCallable<void> | (() => unknown);
10+
type Hooks<Props> = {
11+
mounted?: EventCallable<Props> | EventCallable<void> | ((props: Props) => unknown);
1212
unmounted?: EventCallable<void> | (() => unknown);
1313
};
1414

@@ -26,7 +26,7 @@ type BindFromProps<Props> = {
2626
| ((...args: Parameters<Props[K]>) => ReturnType<Props[K]>)
2727
// Edge-case: allow to pass an event listener without any parameters (e.g. onClick: () => ...)
2828
| (() => ReturnType<Props[K]>)
29-
// Edge-case: allow to pass an Store, which contains a function
29+
// Edge-case: allow to pass a Store, which contains a function
3030
| Store<Props[K]>
3131
: Store<Props[K]> | Props[K];
3232
};
@@ -35,7 +35,7 @@ type BindFromProps<Props> = {
3535
* Computes final props type based on Props of the view component and Bind object.
3636
*
3737
* Props that are "taken" by Bind object are made **optional** in the final type,
38-
* so it is possible to owerrite them in the component usage anyway
38+
* so it is possible to overwrite them in the component usage anyway
3939
*/
4040
type FinalProps<Props, Bind extends BindFromProps<Props>> = Show<
4141
Omit<Props, keyof Bind> & {
@@ -62,7 +62,7 @@ type FinalProps<Props, Bind extends BindFromProps<Props>> = Show<
6262
export function reflect<Props, Bind extends BindFromProps<Props>>(config: {
6363
view: ComponentType<Props>;
6464
bind: Bind;
65-
hooks?: Hooks;
65+
hooks?: Hooks<Props>;
6666
/**
6767
* This configuration is passed directly to `useUnit`'s hook second argument.
6868
*/
@@ -95,7 +95,7 @@ export function createReflect<Props, Bind extends BindFromProps<Props>>(
9595
): (
9696
bind: Bind,
9797
features?: {
98-
hooks?: Hooks;
98+
hooks?: Hooks<Props>;
9999
/**
100100
* This configuration is passed directly to `useUnit`'s hook second argument.
101101
*/
@@ -143,7 +143,7 @@ export function list<
143143
bind?: Bind;
144144
mapItem?: MapItem;
145145
getKey?: (item: Item) => React.Key;
146-
hooks?: Hooks;
146+
hooks?: Hooks<Props>;
147147
/**
148148
* This configuration is passed directly to `useUnit`'s hook second argument.
149149
*/
@@ -155,7 +155,7 @@ export function list<
155155
bind?: Bind;
156156
mapItem: MapItem;
157157
getKey?: (item: Item) => React.Key;
158-
hooks?: Hooks;
158+
hooks?: Hooks<Props>;
159159
/**
160160
* This configuration is passed directly to `useUnit`'s hook second argument.
161161
*/
@@ -200,7 +200,7 @@ export function variant<
200200
cases: Partial<Record<CaseType, ComponentType<Props>>>;
201201
default?: ComponentType<Props>;
202202
bind?: Bind;
203-
hooks?: Hooks;
203+
hooks?: Hooks<Props>;
204204
/**
205205
* This configuration is passed directly to `useUnit`'s hook second argument.
206206
*/
@@ -211,7 +211,7 @@ export function variant<
211211
then: ComponentType<Props>;
212212
else?: ComponentType<Props>;
213213
bind?: Bind;
214-
hooks?: Hooks;
214+
hooks?: Hooks<Props>;
215215
/**
216216
* This configuration is passed directly to `useUnit`'s hook second argument.
217217
*/

src/core/list.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export function listFactory(context: Context) {
2020
[K in keyof Props]: (item: Item, index: number) => Props[K];
2121
};
2222
getKey?: (item: Item) => React.Key;
23-
hooks?: Hooks;
23+
hooks?: Hooks<Props>;
2424
useUnitConfig?: UseUnitConifg;
2525
}): React.FC {
2626
const ItemView = reflect<Props, Bind>({

src/core/reflect.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
import { Effect, Event, EventCallable, is, scopeBind, Store } from 'effector';
1+
import { Effect, Event, is, scopeBind, Store } from 'effector';
22
import { useProvidedScope } from 'effector-react';
3-
import React from 'react';
3+
import React, { PropsWithoutRef, RefAttributes } from 'react';
44

5-
import { BindProps, Context, Hook, Hooks, UseUnitConifg, View } from './types';
5+
import { BindProps, Context, Hooks, UseUnitConifg, View } from './types';
66

77
export interface ReflectConfig<Props, Bind extends BindProps<Props>> {
88
view: View<Props>;
99
bind: Bind;
10-
hooks?: Hooks;
10+
hooks?: Hooks<Props>;
1111
useUnitConfig?: UseUnitConifg;
1212
}
1313

@@ -25,11 +25,11 @@ export function reflectCreateFactory(context: Context) {
2525
export function reflectFactory(context: Context) {
2626
return function reflect<Props, Bind extends BindProps<Props> = BindProps<Props>>(
2727
config: ReflectConfig<Props, Bind>,
28-
): React.ExoticComponent<{}> {
28+
): React.ExoticComponent<PropsWithoutRef<Props> & RefAttributes<unknown>> {
2929
const { stores, events, data, functions } = sortProps(config.bind);
3030
const hooks = sortProps(config.hooks || {});
3131

32-
return React.forwardRef((props, ref) => {
32+
return React.forwardRef((props: Props, ref) => {
3333
const storeProps = context.useUnit(stores, config.useUnitConfig);
3434
const eventsProps = context.useUnit(events as any, config.useUnitConfig);
3535
const functionProps = useBoundFunctions(functions);
@@ -47,10 +47,10 @@ export function reflectFactory(context: Context) {
4747
const functionsHooks = useBoundFunctions(hooks.functions);
4848

4949
React.useEffect(() => {
50-
const hooks: Hooks = Object.assign({}, functionsHooks, eventsHooks);
50+
const hooks: Hooks<Props> = Object.assign({}, functionsHooks, eventsHooks);
5151

5252
if (hooks.mounted) {
53-
hooks.mounted();
53+
hooks.mounted(props);
5454
}
5555

5656
return () => {

src/core/types.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,14 @@ export type BindProps<Props> = {
2020
[K in keyof Props]: Props[K] | Store<Props[K]> | EventCallable<void>;
2121
};
2222

23-
export type Hook = (() => any) | EventCallable<void> | Effect<void, any, any>;
23+
export type Hook<Props> =
24+
| ((props: Props) => any)
25+
| EventCallable<Props>
26+
| Effect<Props, any, any>;
2427

25-
export type Hooks = {
26-
mounted?: Hook;
27-
unmounted?: Hook;
28+
export type Hooks<Props> = {
29+
mounted?: Hook<Props>;
30+
unmounted?: Hook<void>;
2831
};
2932

3033
export type UseUnitConifg = Parameters<typeof useUnit>[1];

src/core/variant.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@ export function variantFactory(context: Context) {
1919
source: Store<Variant>;
2020
bind?: Bind;
2121
cases: Record<Variant, View<Props>>;
22-
hooks?: Hooks;
22+
hooks?: Hooks<Props>;
2323
default?: View<Props>;
2424
useUnitConfig?: UseUnitConifg;
2525
}
2626
| {
2727
if: Store<boolean>;
2828
then: View<Props>;
2929
else?: View<Props>;
30-
hooks?: Hooks;
30+
hooks?: Hooks<Props>;
3131
bind?: Bind;
3232
useUnitConfig?: UseUnitConifg;
3333
},

src/no-ssr/reflect.test.tsx

+65-2
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ test('InputCustom [replace value]', async () => {
7171
});
7272

7373
// Example 2 (InputBase)
74-
const InputBase: FC<InputHTMLAttributes<HTMLInputElement>> = (props) => {
74+
type InputBaseProps = InputHTMLAttributes<HTMLInputElement>;
75+
const InputBase: FC<InputBaseProps> = (props) => {
7576
return <input {...props} />;
7677
};
7778

@@ -375,6 +376,37 @@ describe('hooks', () => {
375376
expect(scope.getState($isMounted)).toBe(true);
376377
});
377378

379+
test('callback with props', () => {
380+
const mounted = createEvent<InputBaseProps>();
381+
const $lastProps = restore(mounted, null);
382+
383+
const $value = createStore('test');
384+
385+
const scope = fork();
386+
387+
const Name = reflect({
388+
view: InputBase,
389+
bind: {
390+
value: $value,
391+
},
392+
hooks: {
393+
mounted: (props: InputBaseProps) => mounted(props),
394+
},
395+
});
396+
397+
render(
398+
<Provider value={scope}>
399+
<Name data-testid="name" />
400+
</Provider>,
401+
);
402+
403+
expect($lastProps.getState()).toBeNull();
404+
expect(scope.getState($lastProps)).toStrictEqual({
405+
value: 'test',
406+
'data-testid': 'name',
407+
});
408+
});
409+
378410
test('event', () => {
379411
const changeName = createEvent<string>();
380412
const $name = restore(changeName, '');
@@ -419,10 +451,41 @@ describe('hooks', () => {
419451
expect($isMounted.getState()).toBe(false);
420452
expect(scope.getState($isMounted)).toBe(true);
421453
});
454+
455+
test('event with props', () => {
456+
const mounted = createEvent<InputBaseProps>();
457+
const $lastProps = restore(mounted, null);
458+
459+
const $value = createStore('test');
460+
461+
const scope = fork();
462+
463+
const Name = reflect({
464+
view: InputBase,
465+
bind: {
466+
value: $value,
467+
},
468+
hooks: { mounted },
469+
});
470+
471+
render(
472+
<Provider value={scope}>
473+
<Name data-testid="name" />
474+
</Provider>,
475+
);
476+
477+
expect($lastProps.getState()).toBeNull();
478+
expect(scope.getState($lastProps)).toStrictEqual({
479+
value: 'test',
480+
'data-testid': 'name',
481+
});
482+
});
422483
});
423484

424485
describe('unmounted', () => {
425-
const changeVisible = createEffect<boolean, void>({ handler: () => {} });
486+
const changeVisible = createEffect<boolean, void>({
487+
handler: () => {},
488+
});
426489
const $visible = restore(
427490
changeVisible.finally.map(({ params }) => params),
428491
true,

type-tests/types-reflect.tsx

+52-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable @typescript-eslint/ban-ts-comment */
22
import { reflect } from '@effector/reflect';
33
import { createEvent, createStore } from 'effector';
4-
import React, { ComponentType, PropsWithChildren, ReactNode } from 'react';
4+
import React, { ComponentType, FC, PropsWithChildren, ReactNode } from 'react';
55
import { expectType } from 'tsd';
66

77
// basic reflect
@@ -374,3 +374,54 @@ function localize(value: string): unknown {
374374

375375
const Test: ComponentType<{ value: string; children: ReactNode }> = Input;
376376
}
377+
378+
// reflect supports mounted as EventCallable<void>
379+
{
380+
type Props = { loading: boolean };
381+
382+
const mounted = createEvent();
383+
384+
const Foo: FC<Props> = (props) => <></>;
385+
386+
const $loading = createStore(true);
387+
388+
const Bar = reflect({
389+
view: Foo,
390+
bind: {
391+
loading: $loading,
392+
},
393+
hooks: { mounted },
394+
});
395+
}
396+
397+
// reflect supports mounted as EventCallable<Props>
398+
{
399+
type Props = { loading: boolean };
400+
401+
const mounted = createEvent<Props>();
402+
403+
const Foo: FC<Props> = (props) => <></>;
404+
405+
const $loading = createStore(true);
406+
407+
const Bar = reflect({
408+
view: Foo,
409+
bind: {
410+
loading: $loading,
411+
},
412+
hooks: { mounted },
413+
});
414+
}
415+
416+
// should error if mounted event doesn't satisfy component props
417+
{
418+
const mounted = createEvent<{ foo: string }>();
419+
420+
const Foo: FC<{ bar: number }> = () => null;
421+
422+
const Bar = reflect({
423+
view: Foo,
424+
// @ts-expect-error
425+
hooks: { mounted },
426+
});
427+
}

0 commit comments

Comments
 (0)