Consuming Variables

In Flowgram, managing and consuming variables is at the core of building dynamic, interactive applications.

Official Material: VariableSelector

To make it easier for you to integrate variable selection functionality into your application, the official materials provide the VariableSelector component. For details, please read the documentation: VariableSelector

Getting the Variable Tree within a Node

In the nodes on the canvas, we often need to get the variables available in the current scope and display them in a tree structure for users to select and operate.

useAvailableVariables

useAvailableVariables is a lightweight Hook that directly returns an array of available variables (VariableDeclaration[]) in the current scope.

use-variable-tree.tsx
import {
  type BaseVariableField,
  useAvailableVariables,
} from '@flowgram.ai/fixed-layout-editor';

// .... In a React component or Hook

const availableVariables = useAvailableVariables();

const renderVariable = (variable: BaseVariableField) => {
  // You can render each variable according to your needs here
  // ....
}

return availableVariables.map(renderVariable);

// ....

Drilling Down into Object Type Variables

When a variable's type is Object, we often need to "drill down" into it to get its properties. The ASTMatch.isObject method can help us determine if a variable's type is an object. If it is, we can recursively render its properties.

use-variable-tree.tsx
import {
  type BaseVariableField,
  ASTMatch,
} from '@flowgram.ai/fixed-layout-editor';

// ....

const renderVariable = (variable: BaseVariableField) => ({
  title: variable.meta?.title,
  key: variable.key,
  // Only Object type variables can be drilled down
  children: ASTMatch.isObject(variable.type) ? variable.type.properties.map(renderVariable) : [],
});

// ....

Drilling Down into Array Type Variables

Similar to the Object type, when we encounter an Array type variable, we also want to display its internal structure. For arrays, we are usually concerned with the type of its elements. ASTMatch.isArray can determine if a variable's type is an array. It's worth noting that the element type of an array can be anything, even another array. Therefore, we need a recursive helper function getTypeChildren to handle this situation.

use-variable-tree.tsx
import {
  type BaseVariableField,
  type BaseType,
  ASTMatch,
} from '@flowgram.ai/fixed-layout-editor';

// ....

const getTypeChildren = (type?: BaseType): BaseVariableField[] => {
  if (!type) return [];

  // Get the properties of an Object
  if (ASTMatch.isObject(type)) return type.properties;

  // Recursively get the element type of an Array
  if (ASTMatch.isArray(type)) return getTypeChildren(type.items);

  return [];
};

const renderVariable = (variable: BaseVariableField) => ({
  title: variable.meta?.title,
  key: variable.key,
  children: getTypeChildren(variable.type).map(renderVariable),
});

// ....

ScopeAvailableData

The ScopeAvailableData object is one of the core components of the variable system. It is returned by the useScopeAvailable Hook and is your main bridge for interacting with the available variables in the scope.

useScopeAvailable

useScopeAvailable returns a ScopeAvailableData object, which not only contains all the available variable information in the current scope but also provides some advanced APIs, such as trackByKeyPath.

Its main differences from useAvailableVariables are:

  • Return Value: useAvailableVariables directly returns a variable array, while useScopeAvailable returns a ScopeAvailableData object that contains the variables property and other methods.
  • Applicable Scenarios: When you need to perform more complex operations on variables, such as tracking changes to a single variable, useScopeAvailable is your best choice.
import { useScopeAvailable } from '@flowgram.ai/fixed-layout-editor';

const available = useScopeAvailable();

// The available object contains the variable list and other APIs
console.log(available.variables);

Getting the Variable List

The most basic usage is to get all the available variables in the current scope.

import { useScopeAvailable } from '@flowgram.ai/fixed-layout-editor';

function MyComponent() {
  const available = useScopeAvailable();

  // available.variables is an array containing all available variables
  console.log(available.variables);

  return (
    <ul>
      {available.variables.map(variable => (
        <li key={variable.key}>{variable.name}</li>
      ))}
    </ul>
  );
}

trackByKeyPath

When you are only concerned with the changes of a specific variable (especially one nested in an Object or Array), trackByKeyPath comes in handy. It allows you to accurately "subscribe" to the updates of this variable without causing the component to re-render due to changes in other unrelated variables, thus achieving finer performance optimization.

Suppose we have an Object type variable named user with a name property. We want to update the component when user.name changes.

import { useScopeAvailable } from '@flowgram.ai/fixed-layout-editor';
import { useEffect, useState } from 'react';

function UserNameDisplay() {
  const available = useScopeAvailable();
  const [userName, setUserName] = useState('');

  useEffect(() => {
    // Define the variable path we want to track
    const keyPath = ['user', 'name'];

    // Start tracking!
    const disposable = available.trackByKeyPath(keyPath, (nameField) => {
      // This callback function will be triggered when user.name changes
      // nameField is the changed variable field, from which we can get the latest default value
      setUserName(nameField?.meta.default || '');
    });

    // When the component unmounts, don't forget to cancel the tracking to avoid memory leaks
    return () => disposable.dispose();
  }, [available]); // The dependency is the available object

  return <div>User Name: {userName}</div>;
}

Global Listening API

In addition to trackByKeyPath, ScopeAvailableData also provides a set of global variable change event listening APIs that allow you to control the response logic to variable changes more finely.

This is very useful when dealing with complex scenarios that require manual subscription management.

Let's use a table to compare these three core listening APIs in detail:

APITriggerCallback ParametersCore Differences and Applicable Scenarios
onVariableListChangeWhen the list structure of available variables changes.(variables: VariableDeclaration[]) => voidOnly cares about the list itself. For example, an upstream node adds/deletes an output variable, causing the total number or members of available variables to change. It does not care about internal or drilled-down changes to variables. Suitable for scenarios where the UI needs to be updated based on the presence or number of variables in the list.
onAnyVariableChangeWhen the content (meta, type, drilldown fields) of any variable in the list changes.(changedVariable: VariableDeclaration) => voidOnly cares about content (type, meta, drilldown) updates to variables. For example, a user modifies the type of an output variable. It does not care about changes to the list structure. Suitable for scenarios where you need to react to changes in the content of any variable.
onListOrAnyVarChangeWhen either of the above two situations occurs.(variables: VariableDeclaration[]) => voidThe most comprehensive listener, a combination of the previous two. It is triggered by either a change in the list structure or a change in the value of any variable. Suitable for "catch-all" scenarios where you need to respond to any possible changes.

Code Example

Let's look at a specific example of how to use these APIs in a component.

import { useScopeAvailable } from '@flowgram.ai/fixed-layout-editor';
import { useEffect } from 'react';

function AdvancedListenerComponent() {
  const available = useScopeAvailable();

  useEffect(() => {
    // 1. Listen for changes in the list structure
    const listChangeDisposable = available.onVariableListChange((variables) => {
      console.log('The structure of the available variable list has changed! The new list length is:', variables.length);
    });

    // 2. Listen for changes in the value of any variable
    const valueChangeDisposable = available.onAnyVariableChange((changedVariable) => {
      console.log(`The content of variable '${changedVariable.keyPath.join('.')}' has changed!`);
    });

    // 3. Listen for all changes (structure or content)
    const allChangesDisposable = available.onListOrAnyVarChange((variables) => {
      console.log('The variable list or the content of one of its variables has changed!');
      // Note: The callback parameter here is the complete variable list, not the single changed variable
    });

    // When the component unmounts, be sure to clean up all listeners to prevent memory leaks
    return () => {
      listChangeDisposable.dispose();
      valueChangeDisposable.dispose();
      allChangesDisposable.dispose();
    };
  }, [available]);

  return <div>Please check the console for logs of variable changes...</div>;
}

Key Points:

  • These APIs all return a Disposable object.
  • To avoid memory leaks and unnecessary calculations, you must call its dispose() method in the cleanup function of useEffect to cancel the listener.