Browser Client

Overview

The @interopio/react-hooks library provides custom React hooks for the io.Connect JavaScript libraries - @interopio/browser and @interopio/browser-platform, if you are working on an io.Connect Browser project, or @interopio/desktop, if you are working on an io.Connect Desktop project. The examples below use the @interopio/browser library. The @interopio/react-hooks library allows you to start using io.Connect features in your React apps idiomatically in the context of the React framework.

Prerequisites

The @interopio/react-hooks library comes with the latest version of the @interopio/browser library, but requires the React and ReactDOM libraries to be installed. To install the packages, navigate to the root directory of your project and run:

npm install --save @interopio/react-hooks

Library Features

The @interopio/react-hooks library offers a way to consume the APIs of the @interopio/browser library in your web apps via React Hooks and React Context.

Context

The <IOConnectProvider /> component is a React context provider component. It invokes a factory function (with default or user-defined configuration) which initializes the @interopio/browser library. The io object returned by the factory function is set as the context value.

The following example demonstrates the signature of the <IOConnectProvider /> component:

interface IOConnectProviderProps {
    children: ReactNode;
    settings: IOConnectInitSettings;
    fallback?: NonNullable<ReactNode> | null;
    onInitError?: (error: Error) => void;
}

interface IOConnectInitSettings {
    browser?: {
        config?: IOConnectBrowser.Config;
        factory?: IOConnectBrowserFactoryFunction;
    };
    browserPlatform?: {
        config?: IOConnectBrowserPlatform.Config;
        factory?: IOConnectBrowserPlatformFactoryFunction;
    };
    desktop?: {
        config?: IOConnectDesktop.Config;
        factory?: IOConnectDesktopFactory;
    };
}

const IOConnectProvider: FC<IOConnectProviderProps>;

The following table describes the properties of the IOConnectInitSettings object:

Property Type Description
browser object Object with two properties: config and factory. The config property accepts a configuration object for the @interopio/browser library. The factory property accepts the factory function exposed by the @interopio/browser library. You should define this object if your app is a Browser Client app in the context of io.Connect Browser.
browserPlatform object Object with two properties: config and factory. The config property accepts a configuration object for the @interopio/browser-platform library. The factory property accepts the factory function exposed by the @interopio/browser-platform library. You should define this object if your app is a Main app app in the context of io.Connect Browser.
desktop object Object with two properties: config and factory. The config property accepts a configuration object for the @interopio/desktop library used in io.Connect Desktop. The factory property accepts the factory function exposed by the @interopio/desktop library. You should define this object if your app is an io.Connect Desktop app.

⚠️ Note that you can't define a browser and browserPlatform property at the same time, but you can define one of them together with desktop. This is useful if you want your app to have different initialization characteristics in io.Connect Browser and io.Connect Desktop.

All properties are optional, but it's recommended that you provide the factory functions explicitly. If no factory functions are provided, the library will try to select the appropriate function injected in the global window object.

The following table describes the properties of the IOConnectProviderProps object:

Property Description
children Required. React components which may contain io.Connect-related logic.
fallback React component to display while initializing io.Connect.
onInitError Callback that will be invoked if the io.Connect library initialization fails. Available since io.Connect Browser 3.3.
settings Required. Settings object containing the desired factory functions and configuration objects.

IOConnectContext is the React context which is used by the <IOConnectProvider /> component. You can consume this context from anywhere inside you app with the default React hook useContext().

const IOConnectContext: Context<IOConnectBrowser.API | IOConnectDesktop.API>;

Hooks

The useIOConnect() hook is a React hook which will invoke the callback that you pass to it.

The following example demonstrates the signature of useIOConnect():

const useIOConnect: <K = IOConnectBrowser.API | IOConnectDesktop.API, T = void>(
    cb: (io: K, ...dependencies: any[]) => T | Promise<T>,
    dependencies?: any[]
) => T;
Parameter Description
cb Required. Async or sync callback function that will be invoked with the io object and an array of user-defined dependencies. The callback may or may not include io.Connect-related code.
dependencies Array of user-defined variables that will trigger the invocation of the provided callback based on whether the value of any of the specified variables has changed (same functionality as the useEffect() React hook).

The useIOConnectInit() hook is a React hook which initializes the provided io.Connect JavaScript library. It accepts a required IOConnectInitSettings object as a first argument and an optional onInitError callback as a second that will be invoked if the io.Connect library initialization fails.

type UseIOInitFunc = (
    settings: IOConnectInitSettings,
    // Available since io.Connect Browser 3.3.
    onInitError?: (error: Error) => void
) => IOConnectBrowser.API | IOConnectDesktop.API;

Usage

The following examples demonstrate using the @interopio/react-hooks library.

Initialization

To access the io.Connect APIs, initialize and optionally configure the @interopio/browser library. You can do this in two ways - by using the <IOConnectProvider /> component or the useIOConnectInit() hook. The difference is that the <IOConnectProvider /> initializes the @interopio/browser library and makes the returned io API object globally available by automatically assigning it as a value to IOConnectContext, while the useIOConnectInit() hook initializes the library and returns an io API object which you then have to make available to your other components by passing it as a prop, by creating a context, or by attaching it to the global window object.

⚠️ Note that the @interopio/react-hooks library is just a thin wrapper designed to work with the @interopio/browser and the @interopio/desktop library. For that reason, if you are using React with TypeScript, you should type cast the initialized io.Connect API object (io) to the appropriate type. The default type is IOConnectBrowser.API | IOConnectDesktop.API.

Add the <IOConnectProvider /> component by wrapping your other components inside it (preferably the root one). Pass the settings object to the <IOConnectProvider />. It will initialize the @interopio/browser library and make the io.Connect APIs available in your app by setting the returned io object as the value of IOConnectContext:

import IOBrowser from "@interopio/browser";
import { IOConnectProvider } from "@interopio/react-hooks";

ReactDOM.render(
    // Wrap your root component in the `<IOConnectProvider />` in order
    // to be able to access the io.Connect APIs from all child components.
    <IOConnectProvider fallback={<h2>Loading...</h2>} settings={{ browser: { factory: IOBrowser } }}>
        <App />
    </IOConnectProvider>,
    document.getElementById("root")
);

You can also initialize the @interopio/browser library with the useIOConnectInit() hook. The following example demonstrates conditional rendering of a component based on whether the io.Connect API is available:

import IOWorkspaces from "@interopio/workspaces-api";
import { useIOConnectInit } from "@interopio/react-hooks";

const App = () => {
    // Example custom configuration for the io.Connect library.
    const settings = {
        browser: {
            config: {
                libraries: [IOWorkspaces]
            }
        }
    };

    const io = useIOConnectInit(settings);

    return io ? <Main io={io} /> : <Loader />;
};

export default App;

Remember that when you initialize the @interopio/browser library with the useIOConnectInit() hook, you must provide the io object to your nested components to be able the use the io.Connect APIs in them. For example, use React Context or attach it to the global window object.

Consuming io.Connect APIs

After the @interopio/browser library has been successfully initialized, you can access the io.Connect APIs with the built-in React hook useContext() and passing IOConnectContext as its argument, or with the useIOConnect() hook.

⚠️ Note that this library is just a thin wrapper designed to work with both @interopio/browser and @interopio/desktop. For that reason, if you are using React with TypeScript, you should type cast the initialized io object to the appropriate type, because the default type is IOConnectBrowser.API | IOConnectDesktop.API.

The following example demonstrates accessing the io object with IOConnectContext and using the Shared Contexts API to get the context of the current window:

import { useContext, useState, useEffect } from "react";
import { IOConnectContext } from "@interopio/react-hooks";

const App = () => {
    const [context, setContext] = useState({});
    // Access the io.Connect APIs by using the `io` object
    // assigned as a value to `IOConnectContext` by the `<IOConnectProvider />` component.
    const io = useContext(IOConnectContext);

    useEffect(() => {
        setContext(await io.windows.my().getContext());
    }, []);

    return (
        <div>
            <h2>My Window Context</h2>
            <pre>{JSON.stringify(context, null, 4)}</pre>
        </div>
    );
};

export default App;

The following example demonstrates accessing the io object with the useIOConnect() hook and using the Window Management API to open an app in a new window on button click:

import { useIOConnect } from "@interopio/react-hooks";

const App = () => {
    const openWindow = useIOConnect(io => (name, url) => {
        io.windows.open(name, url);
    });

    return (
        <table>
            <tr>
                <td>Client List</td>
                <td>
                    <button
                        onClick={() => {
                            openWindow("ClientList", "http://localhost:8080/client-list");
                        }}
                    >
                        Start
                    </button>
                </td>
            </tr>
        </table>
    );
};

export default App;

The following example demonstrates using the Interop API to get the window title through an already registered Interop method:

import { useIOConnect } from "@interopio/react-hooks";
import { useState } from "react";

const App = () => {
    const [title, setTitle] = useState("");
    const getTitle = useIOConnect(io => methodName => {
        io.interop.invoke(methodName).then(r => setTitle(r.returned._result));
    });
    return (
        <>
            <h2>{title}</h2>
            <button
                onClick={() => {
                    getTitle("MyMethods.GetTitle");
                }}
            >
                Get Title
            </button>
        </>
    );
};

export default App;

io.Connect API Usage Recommendations

Consider the following io.Connect API specifics and usage recommendations in relation to the lifecycle of the React components and hooks.

The initialized io.Connect API object (io) will be exactly the same in every re-render of a React component and won't cause re-evaluation of the code if used in a dependency array of a React hook.

When registering Interop methods in your React app, consider the lifecycle of the React components and design the method registration and unregistration process accordingly. If a component attempts to register the same Interop method more than once (e.g., when re-rendered because of props or state updates) without unregistering it first, this will cause the io.Connect API to throw an error. The same will happen if you attempt to register the same Interop method from different components in your React app.

If your component will be rendered more than once, you can use the useEffect() hook to handle the registration and unregistration of your Interop method. When using useEffect() (or any other React hook), consider passing an empty dependency array, or a minimal amount of dependencies in order to avoid unnecessary cycles of registering and unregistering Interop methods because of component re-renders. If you need certain Interop methods to be available throughout the entire lifetime of your app, you should register them in components that will never be unmounted.

The following example demonstrates registering an Interop method using the useEffect() hook:

import { useContext, useEffect } from "react";
import { IOConnectContext } from "@interopio/react-hooks";

const App = () => {
    const io = useContext(IOConnectContext);

    // Registering an Interop method in a `useEffect()` hook.
    useEffect(() => {​
        const methodName = "Addition";
        const methodHandler = (a, b) => a + b;
        const registerMethod = async () => await io.interop.register(methodName, methodHandler)​;
        const unregisterMethod = () => io.interop.unregister(methodName);

        registerMethod();
    ​
        // Return a cleanup function that will unregister the Interop method when the component unmounts.
        return unregisterMethod;

    // Using an empty dependency array to instruct the hook to run only once on initial render
    // to avoid unnecessary cycles of method registration and unregistration during re-renders.
    }, [])
};

export default App;

When subscribing for updates of any io.Connect context type (e.g., shared context, Channel context, Workspace context, window context), consider the lifecycle of the React components and design the subscribing and unsubscribing process accordingly. Keep in mind that the io.Connect API won't prevent the same component from subscribing to the same context object multiple times. Each time the context is updated, all registered update handlers will be invoked. This means that if your component will be rendered more than once (e.g., because of props or state updates), you should unsubscribe from receiving context updates when the component is unmounted. This will avoid potential performance problems caused by creating too many subscriptions because of re-renders.

You can use the useEffect() hook to handle subscribing and unsubscribing for context updates. When using useEffect() (or any other React hook), consider passing an empty dependency array, or a minimal amount of dependencies in order to avoid unnecessary cycles of subscribing and unsubscribing for context updates. If you need certain subscriptions to contexts to be available throughout the entire lifetime of your app, you should subscribe from components that will never be unmounted.

The following example demonstrates subscribing to a shared context using the useEffect() hook:

import { useContext, useEffect } from "react";
import { IOConnectContext } from "@interopio/react-hooks";

const App = () => {
    const io = useContext(IOConnectContext);

    // Subscribing to a shared context in a `useEffect()` hook.
    useEffect(() => {​
        let unsubscribe;
        const contextName = "MyContext";
        const subscriptionHandler = (update) => console.log(`Context data was updated: ${update}`);
        const subscribe = async () => {
            unsubscribe = await io.contexts.subscribe(contextName, subscriptionHandler)​;
        };

        subscribe();
    ​
        // Return a cleanup function that will unsubscribe from the context when the component unmounts.
        return unsubscribe;

    // Using an empty dependency array to instruct the hook to run only once on initial render
    // to avoid unnecessary cycles of subscribing and unsubscribing during re-renders.
    }, [])
};

export default App;

Testing

You can use your own factory function for initializing the @interopio/browser library. This is useful in Jest/Enzyme tests when you want to mock the io.Connect library:

import { mount } from "enzyme";
import { IOConnectProvider } from "@interopio/react-hooks";

// Define a factory function which will mock the io.Connect library.
const ioConnectFactory = () => {
    const ioConnectObject = {
        interop: { invoke: jest.fn(), register: jest.fn() },
        contexts: { subscribe: jest.fn(), update: jest.fn() },
        windows: { open: jest.fn(), my: jest.fn() }
    };

    return Promise.resolve(ioConnectObject);
};

describe("Mock io.Connect", () => {
    it("Should mock the io.Connect library.", () => {
        const wrapper = mount(
        // Pass your factory function to the `<IOConnectProvider />` component.
        <IOConnectProvider settings={{ browser: { factory: ioConnectFactory} }}>
            <App />
        </IOConnectProvider>
        );
        // Your logic here.
    });
});

For additional information on testing React hooks, see the @testing-library/react-hooks library.