Ably Client Undefined
I have the endpoint below which creates a token request object and sends it to the frontend.
import { RequestWithUser } from "@/utils/middlewares/auth-middleware";
import ablyClient from "@/utils/web-socket/ably-client";
import { TokenParams } from "ably";
import { Response } from "express";
// Validate API key at startup
const API_KEY = process.env.ABLY_API_KEY;
if (!API_KEY) {
console.error("Ably API Key required.");
process.exit(1);
}
// Constants for token configuration
const TOKEN_TTL_MS = 3600000; // 1 hour
const getWebSocketCredentialsController = async (req: RequestWithUser, res: Response) => {
const { user } = req;
if (!user) {
return res.status(401).json({ error: 'Unauthorized access.' });
}
const tokenParams: TokenParams = {
clientId: `user-${user.id}`,
capability: {
'notifications': ['subscribe'],
'user:*': ['subscribe', 'publish']
},
ttl: TOKEN_TTL_MS
};
try {
const tokenRequestData = await ablyClient.auth.createTokenRequest(tokenParams);
return res.json(tokenRequestData);
} catch (error) {
console.error("Token request error:", error);
return res.status(500).json({ error: 'Error creating token request' });
}
};
export default getWebSocketCredentialsController;
On the frontend, I have a context which is supposed to fetch the token request and instantiate a client with it: "use client"
import React, { createContext, useCallback, useContext, useEffect, useState } from "react";
import axios from "axios";
import { useAuth } from "./auth-context";
import * as Ably from "ably";
import useResponseInspector from "@/hooks/utils/use-response-inspector";
import { useStorage } from "@/libs/storage";
import { baseURL } from "@/constants/api";
interface WebSocketCredentials {
isLoading: boolean;
error: string | null;
client: Ably.RealtimeClient | undefined;
}
const WebSocketCredentialsContext = createContext<WebSocketCredentials | undefined>(undefined);
export const WebSocketCredentialsProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [client, setClient] = useState<Ably.Realtime | undefined>(undefined);
const { getStringValue } = useStorage()
const { logout, isAuthenticated } = useAuth();
const responseInspector = useResponseInspector();
const fetchCredentials = useCallback(async (): Promise<void> => {
try {
setIsLoading(true);
const token = await getStringValue("jwt");
if (!token) {
logout();
return;
}
const response = await axios.get(`${baseURL}/websocket/credentials`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (response.status === 200) {
const tokenRequest: Ably.TokenRequest = response.data
const client = new Ably.Realtime({authCallback: (tokenParams, callback) => {
callback(null, tokenRequest)}
})
client.connection.on("connected", () => {
console.log("Connected to Ably")
})
client.connection.on("failed", (stateChange) => {
console.error("Failed to connect to Ably", stateChange)
})
console.log("Ably client created", client)
setClient(client)
}
} catch (error) {
if (axios.isAxiosError(error)) {
if (error.response) {
responseInspector(error.response);
}
const errorMessage =
error.response?.data?.error || "An unexpected error occurred. Please try again later.";
setError(errorMessage);
} else {
setError("An unexpected error occurred. Please try again later.");
}
} finally {
setIsLoading(false);
}
}, [logout, responseInspector]);
useEffect(() => {
if (isAuthenticated) {
fetchCredentials();
}
}, [isAuthenticated]);
return (
<WebSocketCredentialsContext.Provider value={{ isLoading, error, client }}>
{children}
</WebSocketCredentialsContext.Provider>
);
};
export const useWebSocket = () => {
const context = useContext(WebSocketCredentialsContext);
if (!context) {
throw new Error("useWebSocketCredentials must be used within a WebSocketCredentialsProvider");
}
return context;
};
In the root layout, I am wappin the children within the context:
import FontAwesome from '@expo/vector-icons/FontAwesome';
import { useFonts } from 'expo-font';
import { Slot} from 'expo-router';
import * as SplashScreen from 'expo-splash-screen';
import { useEffect } from 'react';
import 'react-native-reanimated';
import { AuthProvider } from '@/contexts/auth-context';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { WebSocketCredentialsProvider } from '@/contexts/websocket-credentials';
export {
// Catch any errors thrown by the Layout component.
ErrorBoundary,
} from 'expo-router';
export const unstable_settings = {
// Ensure that reloading on `/modal` keeps a back button present.
initialRouteName: '(app)',
};
// Prevent the splash screen from auto-hiding before asset loading is complete.
SplashScreen.preventAutoHideAsync();
export default function RootLayout() {
const [loaded, error] = useFonts({
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
...FontAwesome.font,
});
// Expo Router uses Error Boundaries to catch errors in the navigation tree.
useEffect(() => {
if (error) throw error;
}, [error]);
useEffect(() => {
if (loaded) {
SplashScreen.hideAsync();
}
}, [loaded]);
if (!loaded) {
return null;
}
return <RootLayoutNav />;
}
function RootLayoutNav() {
return (
<GestureHandlerRootView style={{height: "100%", width: "100%"}}>
<AuthProvider>
<WebSocketCredentialsProvider>
<ScrollProvider>
<Slot />
</ScrollProvider>
</WebSocketCredentialsProvider>
</AuthProvider>
</GestureHandlerRootView>
);
}
Inside of a nested layout, I am warpping the children in the AblyProvider using the client from the context:
import { Stack } from 'expo-router';
import { useAuth } from '@/contexts/auth-context';
import { useTheme } from '@/contexts/theme-context';
import React from 'react';
import { StatusBar } from 'expo-status-bar';
import { AblyProvider } from 'ably/react';
import { useWebSocket } from '@/contexts/websocket-credentials';
export default function Layout() {
const { isAuthenticated } = useAuth();
const { theme, colorScheme } = useTheme()
const { client } = useWebSocket()
const content = () => (
<>
<StatusBar style={colorScheme === "light" ? "dark" : "light"} />
<Stack
screenOptions={{
headerShown: false,
contentStyle: {
backgroundColor: theme.colors.background.secondary,
},
headerStyle: {
backgroundColor: theme.colors.background.secondary,
},
headerTitleStyle: {
color: theme.colors.text.primary,
}
}}
initialRouteName={isAuthenticated ? "(protected)" : "(auth)"}
>
{!isAuthenticated ? (
<Stack.Screen name="(auth)" />
) : (
<Stack.Screen name="(protected)" />
)}
<Stack.Screen name="unsupported-country" />
</Stack>
</>
);
if (client) {
<AblyProvider client={client}>
{content()}
</AblyProvider>
}
return content();
}
Why am I getting an error that the client is undefined when I try to subscribe to channel?
Sorry for the delay, and thank you for sharing this detailed report!
It looks like the issue is with your WebSocketCredentialsProvider. Right now, you’re only initializing your client when you receive the auth callback. Instead, you should properly use the authCallback option and invoke the call directly within it.
Here’s a corrected approach:
export const WebSocketCredentialsProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const { getStringValue } = useStorage();
const { logout, isAuthenticated } = useAuth();
const responseInspector = useResponseInspector();
const fetchCredentials = useCallback(async (): Promise<void> => {
try {
setIsLoading(true);
const token = await getStringValue("jwt");
if (!token) {
logout();
return;
}
const response = await axios.get(`${baseURL}/websocket/credentials`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (response.status === 200) return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
if (error.response) {
responseInspector(error.response);
}
const errorMessage =
error.response?.data?.error || "An unexpected error occurred. Please try again later.";
setError(errorMessage);
} else {
setError("An unexpected error occurred. Please try again later.");
}
} finally {
setIsLoading(false);
}
}, [logout, responseInspector]);
const [client, setClient] = useState<Ably.Realtime>(() =>
new Ably.Realtime({
authCallback: async (data, callback) => {
callback(null, await fetchCredentials());
},
})
);
useEffect(() => {
const connectedListener = () => {
console.log("Connected to Ably");
};
const failedListener = (stateChange) => {
console.error("Failed to connect to Ably", stateChange);
};
client.connection.on("connected", connectedListener);
client.connection.on("failed", failedListener);
return () => {
client.connection.off("connected", connectedListener);
client.connection.off("failed", failedListener);
};
}, [client]);
return (
<WebSocketCredentialsContext.Provider value={{ isLoading, error, client }}>
{children}
</WebSocketCredentialsContext.Provider>
);
};
Let me know if you have any questions or need further clarification. Happy to help!