Skip to main content

Integration

❗️info

Most UI-Frameworks already have an Integration for AgileTs. In general, an Integration makes it possible to use AgileTs in a particular UI-Framework and helps us to bind States to Components. Check here if an Integration for your preferred UI-Framework already exists. If there is no existing Integration yet, this Section might be interesting for you.

The Integration Class serves an Interface to UI-Frameworks like React or Vue. It allows AgileTs to bind States to UI-Components and trigger rerender on them when a bound State value changes.

new Integration({
key: 'myFramework',
frameworkInstance: MyFramework,
bind: () => {
// Will be called during the integration process
// and determines whether the Framework is ready
return Promise.resolve(true);
},
updateMethod: (componentInstance, updatedData) => {
// Will be called on each State value mutation (only in Component based Subscriptions)
// For example, if MY_STATE value mutates from 'jeff' to 'hans'
// Then this method will be called with the following props:
// componentInstance: Component to which the State is subscribed to
// updatedData: Changed data (in our case '{myState: 'hans'}')
//
// Note: Callback based Subscriptions use a callback function
// to cause rerender on Components and therefore don't call this method!
}
});

🛠 Subscriptions

In order to create a well-functioning Integration, we need a basic understanding of how States can be bound/subscribed to Components. In AgileTs, there are two different ways of doing so:

Component based

A Component based Subscription is intended for Components that manage their local states internally in a specific property. For example in a React Class Component the this.state property. Often a rerender is also triggered when the state property got changed. Therefore, we can merge the changed AgileTs State values into the Component state property, to cause a rerender on the Component.

const MY_STATE = createState('hans', {key: 'myState'});
App.subController.subscribeWithSubsArray(
MyComponent, [MY_STATE.observer]
);

If we now mutate the MY_STATE value

MY_STATE.set('jeff');

the updateMethod() defined in the Integration Class will be called with the following properties:

// ..
updateMethod: (componentInstance, updatedData) => {
console.log(componentInstance); // Returns 'MyComponent'
console.log(updatedData); // Returns '{myState: 'jeff'}'
}
// ..

Be aware that each State needs a unique key to be properly mapped in the updatedData object.

updatedData = {
myState: 'jeff',
myDefaultGroup: [{id: 1, name: 'frank'}, {id: 3, name: 'hans'}],
myComputed: "Hello my name is 'jeff'"
}

If a State can't be represented by the updatedData object, it will be omitted. The updateMethod() will be called anyway, even with an empty updateData object. To avoid this problem, we can use the subscribeWithSubsObject() method. There we pass a keymap with a particular key to each Observer/Subscriber instead of an array of Observers.

App.subController.subscribeWithSubsArray(
MyComponent,
{
myState: MY_STATE.observer
}
);

This way, we can ensure that each Agile Sub Instance can be mapped into the updateData object.

Callback based

A Callback based Subscription is intended for Components that don't manage their local states internally or don't have a specific property handling their states. That is why we can't trigger a rerender by mutating a state property. Therefore, we came across another solution. A callback function which triggers a rerender on the particular Component. This callback function will then be called instead of the updateMethd(), whenever a subscribed State mutates.

const MY_STATE = createState('hans', {key: 'myState'});
App.subController.subscribeWithSubsArray(
() => {console.log('Called callback')},
[MY_STATE.observer]
);

If we now mutate the MY_STATE value

MY_STATE.set('jeff');

the defined callback function will be called.

// console: 'Called callback'

💾 Example

In order to get an idea of what an actual Integration might look like. Here are some examples:

🔵 React

Here you can see what the React Integration looks like.

import { Agile, flatMerge, Integration } from '@agile-ts/core';
import { AgileReactComponent } from './hocs/AgileHOC';
import React from 'react';

const reactIntegration = new Integration<typeof React, AgileReactComponent>({
key: 'react',
frameworkInstance: React,
// Used to update State in Class Components (Component based Subscription)
// Note: Functional Components use a Callback based Subscription,
// therefore they don't call 'updateMethod()'
updateMethod(componentInstance, updatedData: Object) {
// Merge changes into State if some Data updated otherwise force rerender
if (Object.keys(updatedData).length !== 0) {
componentInstance.agileProps = flatMerge(
componentInstance.agileProps,
updatedData
);
componentInstance.setState(
flatMerge(componentInstance.state, updatedData)
);
} else {
componentInstance.forceUpdate();
}
},
});

// Register Integration to AgileTs before instantiation
// So that the user don't have to integrate it manually ('App.integration(reactIntegration)')
Agile.initialIntegrations.push(reactIntegration);

export default reactIntegration;

However, to efficiently use AgileTs in Functional and Class Components, we had to create ways to simplify the binding of States to UI-Components. Therefore, we created the useAgile() Hook for Functional Components and the AgileHOC() for Class Components. In the following examples we visually demonstrate the difference of, how much easier e.g. the useAgile() Hook made the binding of States to Components:

  • binding State with useAgile():

    FunctionalComponent.ts
     useAgile(MY_STATE);
  • binding State manually:

    FunctionalComponent.ts
     // Simple reducer to create a 'rerender' callback
    const [, forceRender] = React.useReducer((s) => s + 1, 0);

    useEffect(() => {
    // Create Callback based Subscription to bind State to Component
    // and rerender Component whenever the State mutates
    const subscriptionContainer = App.subController.subscribeWithSubsArray(
    // Our rerender trigger callback
    () => {
    forceRender();
    },
    [MY_STATE.observer]
    );

    // Unsubscribe Callback based Subscription on Unmount
    // -> cleaning up, otherwise it can lead to memory leaks
    return () => {
    App.subController.unsubscribe(subscriptionContainer);
    };
    });

Last but not least, lets take a quick look how a simplified useAgile() Hook looks like.

import React from 'react';
import {
Agile,
getAgileInstance,
extractObservers,
Observer,
State,
} from '@agile-ts/core';

function useAgile(deps: Array<State | Collection | Observer | undefined>, agileInstance?: Agile){
// Extract Observers from passed Agile Sub Instances
// Each State has an Observer, which can be subscribed to a Component
// Through such Observer the State is able to trigger rerenders on the subscribed Components
const depsArray = extractObservers(deps);

// Trigger State, used to force Component to rerender
const [, forceRender] = React.useReducer((s) => s + 1, 0);

useEffect(() => {
// Try to get Agile Instance from passed Instances, otherwise drop error
if (!agileInstance) agileInstance = getAgileInstance(depsArray[0]);
if (!agileInstance || !agileInstance.subController) {
Agile.logger.error('Failed to subscribe Component with deps', depsArray);
return;
}

// Remove undefined Observers, since we can't subscirbe a not existing Observer
const observers: Observer[] = depsArray.filter(
(dep): dep is Observer => dep !== undefined
);

// Create Callback based Subscription
// -> whenever a subscribed State mutates, this callback will be called
// or if its an Component based Subscription the 'updateMethod()' in the React Integration (used in AgileHOC)
const subscriptionContainer = agileInstance.subController.subscribeWithSubsArray(
() => {
forceRender();
},
observers,
key
);

// Unsubscribe Callback based Subscription on Unmount (cleaning up, otherwise it can lead to memory leaks)
return () => {
agileInstance?.subController.unsubscribe(subscriptionContainer);
};
}, []);

// Create return value
return depsArray.map((dep) => {
return dep?.value;
});
}

📭 Props

new Integration(config);

config

An Integration takes a required configuration object as its only parameter.

new Integration<typeof React, AgileReactComponent>({
key: 'myFramework',
frameworkInstance: MyFramework,
bind: () => {},
updateMethod: (componentInstance, updatedData) => {}
});

Here is a Typescript Interface for quick reference. However, each property is explained in more detail below.

export interface CreateIntegrationConfig<F = any, C = any>
extends IntegrationMethods<C> {
key: string;
frameworkInstance?: F;
}

// or without extending

export interface CreateIntegrationConfig<F = any, C = any> {
key: string;
frameworkInstance?: F;
bind?: (agileInstance: Agile) => Promise<boolean>;
updateMethod?: (componentInstance: C, updatedData: Object) => void;
}

key

The required property key/name should be a unique string/number to identify the Integration later.

new Integration({
key: "myIntegration"
// ..
});
TypeDefaultRequired
string \| numberundefinedYes

frameworkInstance

An Instance of the Framework the Integration represents.

new Integration({
frameworkInstance: MyFramework
// ..
});

For example, in the case of React, it should be the React Instance.

TypeDefaultRequired
anyundefinedNo

bind

Will be called during the integration process and determines whether the Framework is ready.

new Integration({
bind: () => {
const isReady = /* Some conditions */;
return Promise.resolve(isReady);
}
// ..
});

For example, it can be used to configure some things before the Framework is integrated into AgileTs.

TypeDefaultRequired
(agileInstance: Agile) => Promise<boolean>undefinedNo

updateMethod

Will be called as soon as a State subscribed to a Component (componentInstance) mutates.

new Integration({
updateMethod: (componentInstance, updatedData) => {
// For example, if MY_STATE value mutates from 'jeff' to 'hans'
// Then this method will be called with the following props:
// componentInstance: Component to which the State is subscribed to
// updatedData: Changed data (for instance '{myState: 'hans'}')
}
// ..
});

Be aware that this is only the case if it is a Component based Subscription. In Callback based Subscription, a callback function will be called to trigger a rerender on the Component instead of the updateMethod().

TypeDefaultRequired
(componentInstance: C, updatedData: Object) => voidundefinedNo

🟦 Typescript

The Integration Class is almost 100% typesafe.