Skip to content
This repository has been archived by the owner on Oct 30, 2023. It is now read-only.

Commit

Permalink
Merge pull request #57 from ably-labs/context
Browse files Browse the repository at this point in the history
feat!: add `<AblyProvider>` and `useAbly`
  • Loading branch information
owenpearson authored Aug 7, 2023
2 parents ea62765 + a3cb538 commit 9b05bea
Show file tree
Hide file tree
Showing 12 changed files with 289 additions and 106 deletions.
74 changes: 62 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,19 +63,41 @@ It is strongly recommended that you use [Token Authentication](https://www.ably.

Once you've added the package using `npm` to your project, you can use the hooks in your `react` code.

Start by adding a reference to the hooks
Start by connecting your app to Ably using the `AblyProvider` component. The options provided to the `AblyProvider` are the same options used for the Ably SDK - and requires either a `string` or an `AblyClientOptions`. You can use this configuration object to setup your API keys, or tokenAuthentication as you normally would. If you want to use the `usePresence` hook, you'll need to explicitly provide a `clientId`.

```javascript
import { configureAbly, useChannel } from "@ably-labs/react-hooks";
```
The `AblyProvider` should be high in your component tree, wrapping every component which needs to access Ably.

Then you need to use the `configureAbly` function to create an instance of the `Ably` JavaScript SDK.

```javascript
configureAbly({ key: "your-ably-api-key", clientId: generateRandomId() });
```jsx
import { AblyProvider } from "@ably-labs/react-hooks";

const options = {
key: "your-ably-api-key",
clientId: "me",
}

root.render(
<AblyProvider options={options}>
<App />
</AblyProvider>
)
```

`configureAbly` matches the method signature of the Ably SDK - and requires either a `string` or an `AblyClientOptions`. You can use this configuration object to setup your API keys, or tokenAuthentication as you normally would. If you want to use the `usePresence` hook, you'll need to explicitly provide a `clientId`.
You may also create your own client and pass it into the context provider.

The `Realtime` constructor provided by the library matches the method signature of the Ably SDK with promises - and requires either a `string` or an `AblyClientOptions`. You can use this configuration object to setup your API keys, or tokenAuthentication as you normally would. If you want to use the `usePresence` hook, you'll need to explicitly provide a `clientId`.

```jsx
import { Realtime } from "@ably-labs/react-hooks";

const client = new Realtime({ key: "your-ably-api-key", clientId: 'me' });

root.render(
<AblyProvider client={client}>
<App />
</AblyProvider>
)
```

Once you've done this, you can use the `hooks` in your code. The simplest example is as follows:

Expand Down Expand Up @@ -239,14 +261,42 @@ interface MyPresenceType {

`PresenceData` is a good way to store synchronised, per-client metadata, so types here are especially valuable.

We also support providing your own `Realtime` instance to `usePresence`, which may be useful if you need to have more than one Ably client on the same page:
### useAbly

The useAbly hook lets you access the Ably client used by the AblyProvider context. This can be useful if you need to access ably-js APIs which aren't available through our react-hooks library.

```javascript
import { useChannel, Realtime } from '@ably-labs/react-hooks'
const client = useAbly();

const realtime = new Realtime(options);
client.authorize();
```

### Usage with multiple clients

If you need to use multiple Ably clients on the same page, the easiest way to do so is to keep your clients in separate `AblyProvider` components. However, if you need to nest `AblyProvider`s, you can pass a string id for each client as a prop to the provider.

```jsx
root.render(
<AblyProvider options={options} id={'providerOne'}>
<AblyProvider options={options} id={'providerTwo'}>
<App />
</AblyProvider>
</AblyProvider>
)
```

This id can then be passed in to each hook to specify which client to use.

```javascript
const ablyContextId = 'providerOne';

const client = useAbly(ablyContextId);

useChannel({ channelName: "your-channel-name", id: ablyContextId }, (message) => {
console.log(message);
});

usePresence({ channelName: "your-channel-name", realtime: realtime }, (presenceUpdate) => {
usePresence({ channelName: "your-channel-name", id: ablyContextId }, (presenceUpdate) => {
...
})
```
11 changes: 1 addition & 10 deletions sample-app/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import { Types } from 'ably';
import React, { useState } from 'react';
import { configureAbly, useChannel, usePresence } from '../../src/index';
import { useChannel, usePresence } from '../../src/index';
import './App.css';

configureAbly({
key: import.meta.env.VITE_ABLY_API_KEY,
clientId: generateRandomId(),
});

function App() {
const [messages, updateMessages] = useState<Types.Message[]>([]);
const [channel, ably] = useChannel('your-channel-name', (message) => {
Expand Down Expand Up @@ -62,8 +57,4 @@ function App() {
);
}

function generateRandomId() {
return Math.random().toString(36).substr(2, 9);
}

export default App;
15 changes: 14 additions & 1 deletion sample-app/src/script.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import * as Ably from 'ably';

import App from './App';
import { AblyProvider } from '../../src/index';

const container = document.getElementById('root')!;

function generateRandomId() {
return Math.random().toString(36).substr(2, 9);
}

const client = new Ably.Realtime.Promise({
key: import.meta.env.VITE_ABLY_API_KEY,
clientId: generateRandomId(),
});

const root = createRoot(container);
root.render(
<React.StrictMode>
<App />
<AblyProvider client={client}>
<App />
</AblyProvider>
</React.StrictMode>
);
91 changes: 91 additions & 0 deletions src/AblyProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import Ably from 'ably';
import { Types } from 'ably';
import React from 'react';

const version = '2.1.1';

const canUseSymbol =
typeof Symbol === 'function' && typeof Symbol.for === 'function';

/**
* Wrapper around Ably.Realtime.Promise which injects the 'react-hooks' agent
*/
export class Realtime extends Ably.Realtime.Promise {
constructor(options: string | Types.ClientOptions) {
let opts: Types.ClientOptions;

if (typeof options === 'string') {
opts = {
key: options,
} as Types.ClientOptions;
} else {
opts = { ...options };
}

(opts as any).agents = { 'react-hooks': version };

super(opts);
}
}

interface AblyProviderProps {
children?: React.ReactNode | React.ReactNode[] | null;
client?: Ably.Types.RealtimePromise;
options?: Ably.Types.ClientOptions;
id?: string;
}

type AblyContextType = React.Context<Realtime>;

// An object is appended to `React.createContext` which stores all contexts
// indexed by id, which is used by useAbly to find the correct context when an
// id is provided.
type ContextMap = Record<string, AblyContextType>;
export const contextKey = canUseSymbol
? Symbol.for('__ABLY_CONTEXT__')
: '__ABLY_CONTEXT__';

const ctxMap: ContextMap =
typeof globalThis !== 'undefined' ? (globalThis[contextKey] = {}) : {};

export function getContext(ctxId = 'default'): AblyContextType {
return ctxMap[ctxId];
}

export const AblyProvider = ({
client,
children,
options,
id = 'default',
}: AblyProviderProps) => {
if (!client && !options) {
throw new Error('No client or options');
}

if (client && options) {
throw new Error('Provide client or options, not both');
}

const realtime: Realtime =
client || new Realtime(options as Ably.Types.ClientOptions);

let context = getContext(id);
if (!context) {
context = ctxMap[id] = React.createContext(realtime);
}

// If options have been provided, the client cannot be accessed after the provider has unmounted, so close it
React.useEffect(() => {
if (options) {
return () => {
realtime.close();
};
}
}, []);

return (
<context.Provider value={client as Ably.Types.RealtimePromise}>
{children}
</context.Provider>
);
};
41 changes: 1 addition & 40 deletions src/AblyReactHooks.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,8 @@
import Ably from 'ably';
import { Types } from 'ably';

export type ChannelNameAndOptions = {
channelName: string;
options?: Types.ChannelOptions;
realtime?: Types.RealtimePromise;
id?: string;
};
export type ChannelParameters = string | ChannelNameAndOptions;

const version = '2.1.1';

let sdkInstance: Realtime | null = null;

export class Realtime extends Ably.Realtime.Promise {
constructor(options: string | Types.ClientOptions) {
if (typeof options === 'string') {
options = {
key: options,
} as Types.ClientOptions;
}

(options as any).agents = { 'react-hooks': version };

super(options);
}
}

export function provideSdkInstance(ablyInstance: Types.RealtimePromise) {
sdkInstance = ablyInstance;
}

export function configureAbly(
ablyConfigurationObject: string | Types.ClientOptions
) {
return sdkInstance || (sdkInstance = new Realtime(ablyConfigurationObject));
}

export function assertConfiguration(): Types.RealtimePromise {
if (!sdkInstance) {
throw new Error(
'Ably not configured - please call configureAbly({ key: "your-api-key", clientId: "someid" });'
);
}

return sdkInstance;
}
16 changes: 16 additions & 0 deletions src/hooks/useAbly.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Types } from 'ably';
import React from 'react';
import { contextKey, getContext } from '../AblyProvider';

export function useAbly(id: string = 'default'): Types.RealtimePromise {
const client = React.useContext(getContext(id)) as Types.RealtimePromise;

if (!client) {
throw new Error(
'Could not find ably client in context. ' +
'Make sure your ably hooks are called inside an <AblyProvider>'
);
}

return client;
}
Loading

0 comments on commit 9b05bea

Please sign in to comment.