reflect
import { reflect } from '@effector/reflect';const Component = reflect({
view: SourceComponent,
bind: Props,
hooks: Hooks,
});Static method to create a component bound to effector stores and events as stores.
Arguments
view— A react component that should be used to bind tobind— Object of effector stores, events or any valuemapProps— Optional object of derived props. Each entry computes a prop for theviewfrom a store value combined with the component's own props.hooks— Optional object{ mounted, unmounted }to handle when component is mounted or unmounted.useUnitConfig- Optional configuration object, which is passed directly to the second argument ofuseUnitfromeffector-react.
Returns
- A react component with bound values from stores and events.
Example
// ./user.tsx
import { reflect } from '@effector/reflect';
import { createEvent, restore } from 'effector';
import React, { ChangeEvent, FC } from 'react';
// Base components
type InputProps = {
value: string;
onChange: ChangeEvent<HTMLInputElement>;
placeholder?: string;
};
const Input: FC<InputProps> = ({ value, onChange, placeholder }) => {
return <input value={value} onChange={onChange} placeholder={placeholder} />;
};
// Model
const changeName = createEvent<string>();
const $name = restore(changeName, '');
const changeAge = createEvent<number>();
const $age = restore(changeAge, 0);
// Components
const Name = reflect({
view: Input,
bind: {
value: $name,
placeholder: 'Name',
onChange: (event) => changeName(event.currentTarget.value),
},
});
const Age = reflect({
view: Input,
bind: {
value: $age,
placeholder: 'Age',
onChange: (event) => changeAge(parseInt(event.currentTarget.value)),
},
});
export const User: FC = () => {
return (
<div>
<Name />
<Age />
</div>
);
};Deriving props with mapProps
Sometimes a prop is not just a store value or a plain value, but a combination of a store's
state and the props passed to the component. For these cases use mapProps.
Each entry is { source, fn }:
source— what the derived value is read from reactively. Likecombine/useUnit, it can be:- a single store —
valueis its state; - an object of stores —
valueis the resolved object ({ a: Store<A> }→{ a: A }); - an array of stores —
valueis the resolved tuple ([Store<A>, Store<B>]→[A, B]).
- a single store —
fn—(value, props) => derivedProp, wherevalueis the resolvedsourcevalue (its type is inferred — no annotation needed) andpropsare the component's own props.
Each key in
mapPropsmust be a prop of theview— a typo'd or unknown key is a type error at the key itself. A key must not appear in bothbindandmapProps— this is a type error. For best type inference, passsourceas an inline literal; a variable typed asStore<any>will widenfn'svalueargument toany.
import { reflect } from '@effector/reflect';
import { createStore } from 'effector';
import React, { FC } from 'react';
type GreetingProps = {
title: string;
label: string;
};
const Greeting: FC<GreetingProps> = ({ label }) => <span>{label}</span>;
const $user = createStore({ name: 'Bob' });
const Hello = reflect({
view: Greeting,
bind: {},
mapProps: {
// `label` is derived from the `$user` store and the incoming `title` prop
label: {
source: $user,
fn: (user, props) => `${props.title} ${user.name}`,
},
},
});
// usage: `label` becomes optional, since it is derived
export const App: FC = () => <Hello title="Hello," />;To derive from several stores at once, pass an object (or array) of stores as source:
const Hello = reflect({
view: Greeting,
bind: {},
mapProps: {
label: {
source: { user: $user, count: $count },
// `s` is inferred as { user: { name: string }; count: number }
fn: (s, props) => `${props.title} ${s.user.name} (${s.count})`,
},
},
});The component re-renders only when the source changes. A prop computed via mapProps
is made optional in the resulting component's type and can still be overridden explicitly at
the usage site (an explicitly passed prop wins over the derived value — in that case fn is
not invoked for the overridden key).
Note: like
bind, themapPropsfield is supported by all Reflect operators —reflect,createReflect,variantandlist. Inlist, a key must not appear in bothmapItemandmapProps—mapItemautomatically omits keys that are derived viamapProps.
Fork API auto-compatibility
This feature is available since 9.0.0 release of Reflect.
The Fork API (opens in a new tab) - is a feature of Effector, which allows to seamlessly create virtualized instances of the application's state and logic called Scope (opens in a new tab)'s.
In React to render an App in specific Scope a Provider (opens in a new tab) component should be used.
When an external system (like React) calls an Effector event and Fork API is used - target Scope should be provided before call via allSettled or scopeBind API.
The reflect operator does it for you under the hood, so you can provide arbitary callbacks into bind:
const Age = reflect({
view: Input,
bind: {
value: $age,
placeholder: 'Age',
onChange: (event) => {
changeAge(parseInt(event.currentTarget.value));
},
},
});☝️ This feature works for bind field in all of Reflect operators.
In most cases your callbacks will be synchronous and you will not need to do anything besides using Provider (opens in a new tab) to set the Scope for the whole React tree.
Special case: Asynchronous callbacks
Asynchronous callbacks are also allowed, but those should follow the rules of Imperative Effect calls with Scope (opens in a new tab) to be compatible.
TypeScript and Polymorphic Types Caveat
Generally, reflect handles polymorphic props well. However, in some implementations, such as Mantine UI (opens in a new tab), it may not work as expected.
In such cases, it is recommended to explicitly narrow the component type. For example:
const ReflectedMantineButton = reflect({
view: MantineButton<'button'>, // <- notice explicit component type in the "<...>" brackets
bind: {
children: 'foo',
onClick: (e) => {
clicked(e.clientX);
},
},
});