@connect
Make sure to first read the introduction to redux in tabris.
The tabris-decorators
module exports connect
, which can be used both as a decorator or as a conventional function. It takes up to two parameters, mapStateToProps
and mapDispatchToProps
, where either can be null
/undefined
. The return value can then be used to connect a custom component or functional component to the globally registered StateProvider
, typically the redux store:
connect(mapStateToProps, mapDispatchToProps)(Original)
connect(mapStateToProps)(Original)
connect(null, mapDispatchToProps)(Original)
Where Original
is a widget or widget factory, and the return value is the same widget or widget factory, except hardwired to the store. Note that the connect
function does not change Original
. Instead it returns a wrapper of Original
with all the same API and features. Therefore the same widget can be connected multiple times to the store in different constellations.
In TypeScript the connect
function should be given a generic type parameter that identifies the widget created by Original
:
connect<Original>(mapStateToProps, mapDispatchToProps)(Original)
This allows the IDE to provide type checks and autocompletion for mapStateToProps
and mapDispatchToProps
.
mapStateToProps
A function that maps the state returned by the store to the properties of the component to connect:
state => properties
The TypeScript type of this function is StateToProps<Component>
, exported by tabris-decorators
.
Example: If the state object of your store has a myString
property, and the widget you want to connect has a text
property, then mapStateToProps
may look like this:
const stateToProps = state => ({
text: state.myString
});
Only properties that actually exist on the connected widget may be given in the returned properties object. A special case is apply
, which will be discussed in the section Usage with functional components.
If mapStateToProps
is not defined “inline” (as part of the connect
call), it is desirable to give it a type. Depending you your project settings it may be required.
This is how you do this in TypeScript:
import {StateToProps} from 'tabris-decorators';
const mapStateToProps: StateToProps<ExampleComponent> =
state => ({
text: state.myString
});
Visual Studio Code supports TypeScript types within JsDoc, so you can also do this in JavaScript:
/** @type {import('tabris-decorators').StateToProps<ExampleComponent>} */
const mapStateToProps = state => ({
text: state.myString
});
mapDispatchToProps
A function that maps the actions accepted by the store to callbacks or events of the component/widget.
dispatch => actionMapper
Where dispatch
is the store method of the same name, and actionMapper
is an object containing callbacks that invoke dispatch
. The TypeScript type of this function is DispatchToProps<Component>
, exported by tabris-decorators
.
Example: If one of the available store actions looks like this:
{
type: 'TOGGLE_STRING',
checked: boolean
}
And your component has a handleToggle
callback property that takes a single boolean parameter like this:
component.handleToggle = checked => doSomething(checked);
Then mapDispatchToProps
may look like this:
const mapDispatchToProps = dispatch => {
handleToggle: checked => dispatch({type: 'TOGGLE_STRING', checked})
};
A more general solution would be to use an equivalent event instead. Your component could implement a onToggle
event like this:
TypeScript:
class ExampleComponent extends Composite {
@event onToggle: Listeners<{target: ExampleComponent, checked: boolean}>;
}
JavaScript (JsDoc optional):
constructor(props) {
/** @type {tabris.Listeners<{target: ExampleComponent, checked: boolean}>} */
this.onToggle = new Listeners(this, 'toggle');
}
Then mapDispatchToProps
may look like this:
const mapDispatchToProps = dispatch => {
onToggle: ({checked}) => dispatch({type: 'TOGGLE_STRING', checked})
};
Only callback properties or events that are actually declared on the connected widget (via a decorator, setter, or property set in the constructor) may be given. A special case is apply
, which will be discussed in the section Usage with functional components.
If mapDispatchToProps
is not defined “inline” (as part of the connect
call), it is desirable to give it a type to get autocompletion in your IDE. Depending you your project settings it may be required.
This is how you do this in TypeScript:
import {DispatchToProps} from 'tabris-decorators';
const mapDispatchToProps: DispatchToProps<ExampleComponent> =
// ...
In Visual Studio Code you can also do this in JavaScript via JsDoc:
/** @type {import('tabris-decorators').DispatchToProps<ExampleComponent>} */
const mapDispatchToProps =
// ...
Usage with built-in widgets
Any core widget of Tabris.js can be connected directly if its visuals and API are sufficient for your use case:
export const ConnectedButton = connect(
state => ({text: 'Some text: ' + state.myString}),
dispatch => ({onSelect: () => dispatch({type: 'TOGGLE_STRING'})})
)(Button);
It can then be used like the Button
constructor:
// with "new"
parent.append(
new ConnectedButton({textColor: 'blue'})
);
// or as a factory
parent.append(
ConnectedButton({textColor: 'blue'})
);
// or JSX
parent.append(
<ConnectedButton textColor='blue'/>
);
Usage with custom components
A custom component (user-defined subclass of Composite
) should be used with connect
mainly for complex UIs that need their own custom state, behavior and/or API in addition to what is handled by the redux store. It may also be appropriate for any UI that exceeds a certain level of complexity and/or extends Page
or Tab
. Note that any internal state that is not synced with the store will not be restored if the store is initialized with persisted state.
As a decorator
If the TypeScript compiler is used (in any TypeScript/TSX or JavaScript/JSX project setup) then connect
can be used as a decorator:
@component
@connect<ExampleComponent>(
state => ({
text: state.myString
}),
dispatch => ({
onToggle: ev => dispatch({type: 'TOGGLE_STRING', checked: ev.checked})
})
)
export class ExampleComponent extends Composite {
@prop text: string;
@event onToggle: Listeners<{target: ExampleComponent, checked: boolean}>;
// Implementation...
}
The order of @component
and @connect
is not relevant. With this syntax ExampleComponent
itself is modified and can not be given to connect
again. If you want to be able to connect the same component to the store in different ways, you need to use connect
as a function:
As a function
This is the necessary approach for plain JavaScript, when the component is not always connected in the same way, or if you do not want to use the decorator syntax.
First define the component class (potentially in another module), then mapDispatchToProps
and mapDispatchToProps
, and then pass all of it to connect
on export.
For plain JavaScript:
exports.ExampleComponent = connect(stateToProps, dispatchToProps)(ExampleComponent);
When asFactory
is applied that needs to be done before connect
.
const connector = connect(stateToProps, dispatchToProps);
exports.ExampleComponent = connector(asFactory(ExampleComponent));
When using the ES6 module syntax, the component needs to have a different local name than the export.
export const ExampleComponent = connect(stateToProps, dispatchToProps)(ExampleComponentBase);
In TypeScript a namespace could be used:
namespace internal {
@component
export class ExampleComponent extends Composite {
// ...
}
}
export const ExampleComponent = connect(stateToProps, dispatchToProps)(internal.ExampleComponent);
export type ExampleComponent = internal.ExampleComponent;
Usage with functional components
A functional component (user-defined widget factory) may be less code than a fully fledged custom component and is a natural fit for the redux pattern.
Styled Component
A simple functional component may take an “attributes” object (containing properties and listeners) and return a pre-configured widget. In this example a button is given a default font
, gets it’s text
from the store state and dispatches actions via onSelect
:
TypeScript/JSX:
export const ConnectedButton = connect(
state => ({text: 'Some text: ' + state.myString}),
dispatch => ({onSelect: () => dispatch({type: 'TOGGLE_STRING'})})
)(
(attributes: Attributes<Button>) =>
<Button font='12px serif' {...attributes}/>
);
JavaScript/JSX:
export const ConnectedButton = connect(
state => ({text: 'Some text: ' + state.myString}),
dispatch => ({onSelect: () => dispatch({type: 'TOGGLE_STRING'})})
)(
/** @param {tabris.Attributes<tabris.Button>} attributes */
attributes => <Button font='12px serif' {...attributes}/>
);
Plain JavaScript:
export const ConnectedButton = connect(
state => ({text: 'Some text: ' + state.myString}),
dispatch => ({onSelect: () => dispatch({type: 'TOGGLE_STRING'})})
)(
/** @param {tabris.Attributes<tabris.Button>} attributes */
attributes => Button({font: '12px serif', ...attributes})
);
Of course mapDispatchToProps
, mapDispatchToProps
(targeting a Button
instance) and the actual factory function can also be defined independently and put together in a separate line:
// const mapStateToProps: StateToProps<Button> = ....
// const mapDispatchToProps: DispatchToProps<Button> = ....
const CustomButton = (attributes: Attributes<Button>) => <Button font='12px serif' {...attributes}/>;
const ConnectedButton = connect(stateToProps, dispatchToProps)(CustomButton);
Composed Functional Component
A functional component may also return composite with children. For this scenario both mapStateToProps
and mapDispatchToProps
support a special pseudo-property apply
. The object given to this property will be treated as a ruleset for the apply
method. It is thereby possible to connect child elements to the store via their given id. It can also be combined by any additional properties applied to the returned widget itself.
This example behaves the same as the previous one, only that the button is wrapped in a composite. The Set
helper function provided by the tabris
module is used to improve provide type safety. However, it could also be omitted.
TypeScript, all in one expression:
export const ConnectedButton = connect(
state => ({
apply: {
'#mybutton': Set(Button, {text: 'Some text: ' + state.myString})
}
}),
dispatch => ({
apply: {
'#mybutton': Set(Button, {onSelect: () => dispatch({type: 'TOGGLE_STRING'})})
}
})
)(
(attributes: Attributes<Composite>) =>
<Composite padding={12} {...attributes}>
<Button id='mybutton' font='12px serif'/>
</Composite>
);
Plain JavaScript, separate expressions:
/** @type {import('tabris-decorators').StateToProps<tabris.Composite>} */
const stateToProps = state => ({
apply: {
'#mybutton': Set(Button, {text: 'Some text: ' + state.myString})
}
});
/** @type {import('tabris-decorators').DispatchToProps<tabris.Composite>} */
const dispatchToProps = dispatch => ({
apply: {
'#mybutton': Set(Button, {onSelect: () => dispatch({type: 'TOGGLE_STRING'})})
}
});
/** @param {tabris.Attributes<tabris.Composite>} attr */
const CustomButton = attr => Composite({padding: 12, ...attr, children: [
Button({id: 'mybutton', font: '12px serif'})
]});
exports.FunctionalComponent = connect(stateToProps, dispatchToProps)(CustomButton);