diff --git a/README.md b/README.md index ec84980..cdd07c9 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,21 @@ A React hook with simple and responsible universal transports. ### Motivation +`use-transport` is a React hook that provides a simple and responsible way to manage data transport. It is designed to be used with [`data-transport`](https://github.com/unadlib/data-transport) to provide a universal transport solution. + +`data-transport` is a generic and responsible communication transporter: + +- iframe +- Broadcast +- Web Worker +- Service Worker +- Shared Worker +- Browser Extension +- Node.js +- WebRTC +- Electron +- More transport port + ### Installation ```bash @@ -18,13 +33,55 @@ yarn add use-transport data-transport ### Features -### Example +- Simple and responsible +- Universal transport +- Support for multiple transport ports +- Support for mock transport +- Full TypeScript support ### API -### Options +You can use the `use-transport` hook to create a transport instance. + +```jsx +import React from 'react'; +import { useTransport } from 'use-transport'; + +const App = () => { + const transport = useTransport('IFrameMain', {}); + + transport.listen( + 'hello', + async () => { + return 'world'; + }, + [] + ); + + const handleClick = async () => { + const response = await transport.emit('ping'); + console.log(response); + }; + + return ; +}; +``` + +#### Parameters + +| Name | Type | Description | +| --------- | ------ | ---------------------- | +| `type` | enums | Transport port type | +| `options` | object | Transport port options | + +#### Returns + +| Name | Type | Description | +| ------------------ | -------- | -------------------------- | +| `transport.emit` | function | Emit a message | +| `transport.listen` | hook | Listen a message with deps | -### Return +> The `use-transport` hook returns a transport instance. more API details can be found in the [data-transport](https://github.com/unadlib/data-transport) documentation. ## License diff --git a/package.json b/package.json index f10ad9c..8ef20bc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "use-transport", - "version": "0.0.1", + "version": "0.1.0", "description": "A React hook with simple and responsible universal transports", "main": "dist/index.cjs.js", "unpkg": "dist/index.umd.js", @@ -33,7 +33,13 @@ "url": "https://github.com/unadlib/use-transport/issues" }, "homepage": "https://github.com/unadlib/use-transport#readme", - "keywords": [], + "keywords": [ + "react", + "hook", + "transport", + "use-transport", + "data-transport" + ], "devDependencies": { "@rollup/plugin-commonjs": "^24.0.1", "@rollup/plugin-node-resolve": "^15.0.1", @@ -46,7 +52,7 @@ "@typescript-eslint/parser": "^7.4.0", "commitizen": "^4.3.0", "coveralls": "^3.1.1", - "data-transport": "^4.3.5", + "data-transport": "^4.3.6", "eslint": "^8.36.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-prettier": "^8.8.0", @@ -82,7 +88,7 @@ }, "peerDependencies": { "@types/react": "^18.0 || ^17.0", - "data-transport": "^4.3.5", + "data-transport": "^4.3.6", "react": "^18.0 || ^17.0" } } diff --git a/src/index.ts b/src/index.ts index bc81dd5..731b4d7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,69 @@ -export const add = (a: number, b: number) => a + b; +import { useEffect, useRef } from 'react'; +import { + type Transport as ITransport, + type BaseInteraction, + createTransport, + type TransportMap, + type TransportOptionsMap, +} from 'data-transport'; + +export type Transport = ITransport & { + listen: ( + /** + * The name of the event to listen for. + */ + name: K, + /** + * The callback to invoke when the event is emitted. + */ + fn: T['listen'][K], + /** + * The dependencies to watch for changes and re-invoke the callback. + */ + deps?: any[] + ) => void; +}; + +/** + * Create a transport instance with the given name and options. + */ +export const useTransport = ( + name: T, + options: TransportOptionsMap[T] +) => { + const transportRef = useRef(null); + if (!transportRef.current) { + transportRef.current = createTransport(name, options); + const { listen } = transportRef.current; + transportRef.current.listen = (( + options, + callback: (...args: unknown[]) => unknown, + deps: unknown[] = [] + ) => { + const listenerRef = useRef<((...args: unknown[]) => unknown) | null>( + null + ); + useEffect(() => { + listenerRef.current = callback; + }, deps); + useEffect(() => { + const removeListener = listen.call( + transportRef.current, + options, + (...args: unknown[]) => listenerRef.current?.(...args) + ); + return () => { + // Remove the listener when the component unmounts. + removeListener?.(); + }; + }, []); + }) as Transport['listen']; + } + useEffect(() => { + return () => { + // Dispose the transport instance when the component unmounts. + transportRef.current?.dispose(); + }; + }, []); + return transportRef.current as Transport; +}; diff --git a/test/index.test.ts b/test/index.test.ts index 6ebd531..e7e7177 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,3 +1,100 @@ -test('', () => { - // +import { act, renderHook } from '@testing-library/react'; +import { useState } from 'react'; +import { + mockPorts, + type Transport as ITransport, + Reverse, + createTransport, +} from 'data-transport'; + +import { useTransport, type Transport } from '../src/index'; + +test('Base', async () => { + const ports = mockPorts(); + type Interaction = { + listen: { + foo: (value: number) => Promise; + }; + emit: { + bar: (value: number) => Promise; + }; + }; + const transport0: ITransport> = createTransport( + 'Base', + ports.create() + ); + const fn0 = jest.fn(); + transport0.listen('bar', async (value) => { + fn0(value); + }); + const fn1 = jest.fn(); + const { result, unmount } = renderHook(() => { + const [state, setState] = useState(0); + const transport: Transport = useTransport('Base', ports.main); + transport.listen( + 'foo', + async (value) => { + const nextState = value + state; + setState(nextState); + fn1(value); + return nextState; + }, + [state] + ); + return { + state, + setState, + transport, + }; + }); + expect(result.current.state).toEqual(0); + + act(() => { + result.current.transport.emit('bar', 1); + }); + expect(result.current.state).toEqual(0); + expect(fn0).toHaveBeenLastCalledWith(1); + + let fooResult = await act(async () => { + return transport0.emit('foo', 1); + }); + expect(result.current.state).toEqual(1); + expect(fn0).toHaveBeenLastCalledWith(1); + expect(fooResult).toEqual(1); + + fooResult = await act(async () => { + return transport0.emit('foo', 2); + }); + expect(result.current.state).toEqual(3); + expect(fn0).toHaveBeenLastCalledWith(1); + expect(fooResult).toEqual(3); + + fooResult = await act(async () => { + return transport0.emit('foo', 3); + }); + expect(result.current.state).toEqual(6); + expect(fn0).toHaveBeenLastCalledWith(1); + expect(fn0).toHaveBeenCalledTimes(1); + expect(fn1).toHaveBeenCalledTimes(3); + expect(fooResult).toEqual(6); + + act(() => { + result.current.transport.emit('bar', 2); + }); + expect(result.current.state).toEqual(6); + expect(fn0).toHaveBeenLastCalledWith(2); + expect(fn0).toHaveBeenCalledTimes(2); + expect(fn1).toHaveBeenCalledTimes(3); + + act(() => { + unmount(); + }); + + act(() => { + transport0.emit('foo', 2); + }); + expect(result.current.state).toEqual(6); + expect(fn0).toHaveBeenLastCalledWith(2); + expect(fn0).toHaveBeenCalledTimes(2); + expect(fn1).toHaveBeenCalledTimes(3); }); diff --git a/yarn.lock b/yarn.lock index 8282590..2fb0e25 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1753,10 +1753,10 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" -data-transport@^4.3.5: - version "4.3.5" - resolved "https://registry.yarnpkg.com/data-transport/-/data-transport-4.3.5.tgz#60430fb6b5d6cb835db5f7ba3fcefdbd9bf78510" - integrity sha512-zB76szk3yWl51eRhemhZIYd9OAHkce4wbo5jbQJAGNjWxkpcKOCoRnxCGyrjFHqqCDUs+KfFH5vQl30ZvRQbDQ== +data-transport@^4.3.6: + version "4.3.6" + resolved "https://registry.yarnpkg.com/data-transport/-/data-transport-4.3.6.tgz#6f51f5d91a9d43e5f4ec1b491d009e80c47e243c" + integrity sha512-YIhrpsHU4xy4BX4RH9cPkDvpF2YmCmVnyV6yJQ79MrSg74mEdMel16tGyL/BkCSZDned0sqBye8jC5fqQxa5PQ== dependencies: uuid "^9.0.0"