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
andbrowserPlatform
property at the same time, but you can define one of them together withdesktop
. 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 isIOConnectBrowser.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 initializedio
object to the appropriate type, because the default type isIOConnectBrowser.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.