Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions packages/react-core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog

## 0.11.4

### Patch Changes

- 98a9464: Fix cache issues in `useMsTeamsChannels`, `useMsTeamsTeams`, and `useSlackChannels` hooks

The cache keys for these hooks now include `tenantId` and `knockChannelId` to ensure that different tenants and Knock channels have separate cache entries. Additionally, the hooks now clear their SWR cache when:
- The tenant ID changes
- The Knock channel ID changes
- The access token is revoked
- The connection status transitions from disconnected/error to connected

This prevents stale data from being displayed when switching between different workspaces, revoking access tokens, or reconnecting.

## 0.11.3

### Patch Changes
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { GetMsTeamsChannelsResponse, MsTeamsChannel } from "@knocklabs/client";
import { useCallback, useEffect, useRef } from "react";
import useSWR from "swr";

import { useKnockClient } from "../../core";
Expand All @@ -23,26 +24,60 @@ function useMsTeamsChannels({
queryOptions,
}: UseMsTeamsChannelsProps): UseMsTeamsChannelsOutput {
const knock = useKnockClient();
const { knockMsTeamsChannelId, tenantId } = useKnockMsTeamsClient();

const fetchChannels = () =>
knock.msTeams.getChannels({
knockChannelId: knockMsTeamsChannelId,
tenant: tenantId,
teamId: teamId!,
queryOptions: {
$filter: queryOptions?.filter,
$select: queryOptions?.select,
},
});
const { knockMsTeamsChannelId, tenantId, connectionStatus } =
useKnockMsTeamsClient();

// Track previous tenant/channel/connectionStatus to detect changes and clear cache
const prevTenantRef = useRef(tenantId);
const prevChannelRef = useRef(knockMsTeamsChannelId);
const prevConnectionStatusRef = useRef(connectionStatus);

const fetchChannels = useCallback(
() =>
knock.msTeams.getChannels({
knockChannelId: knockMsTeamsChannelId,
tenant: tenantId,
teamId: teamId!,
queryOptions: {
$filter: queryOptions?.filter,
$select: queryOptions?.select,
},
}),
[knock.msTeams, knockMsTeamsChannelId, tenantId, teamId, queryOptions],
);

// Include tenantId and knockMsTeamsChannelId in the cache key so that
// SWR treats different tenants as different cache entries
const { data, isLoading, isValidating, mutate } =
useSWR<GetMsTeamsChannelsResponse>(
teamId ? [QUERY_KEY, teamId] : null,
teamId && connectionStatus === "connected"
? [QUERY_KEY, tenantId, knockMsTeamsChannelId, teamId]
: null,
fetchChannels,
{ revalidateOnFocus: false },
);

// Clear cache when tenant, channel, or connection status changes
// This ensures that when the user disconnects and reconnects (possibly to a different
// MS Teams workspace), or when the access token is revoked, the cached channels are cleared
useEffect(() => {
const tenantChanged = prevTenantRef.current !== tenantId;
const channelChanged = prevChannelRef.current !== knockMsTeamsChannelId;
// Detect when connection is re-established (was not connected, now is connected)
const wasConnected = prevConnectionStatusRef.current === "connected";
const isConnected = connectionStatus === "connected";
const connectionReestablished = !wasConnected && isConnected;

if (tenantChanged || channelChanged || connectionReestablished) {
// Reset the SWR state to clear cached data
mutate(undefined, { revalidate: false });
}

prevTenantRef.current = tenantId;
prevChannelRef.current = knockMsTeamsChannelId;
prevConnectionStatusRef.current = connectionStatus;
}, [tenantId, knockMsTeamsChannelId, connectionStatus, mutate]);

return {
data: data?.ms_teams_channels ?? [],
isLoading: isLoading || isValidating,
Expand Down
112 changes: 81 additions & 31 deletions packages/react-core/src/modules/ms-teams/hooks/useMsTeamsTeams.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { GetMsTeamsTeamsResponse, MsTeamsTeam } from "@knocklabs/client";
import { useEffect, useMemo } from "react";
import { useCallback, useEffect, useMemo, useRef } from "react";
import useSWRInfinite from "swr/infinite";

import { useKnockClient } from "../../core";
Expand All @@ -20,25 +20,9 @@ type UseMsTeamsTeamsOutput = {
refetch: () => void;
};

type QueryKey = [key: string, skiptoken: string] | null;

function getQueryKey(
pageIndex: number,
previousPageData: GetMsTeamsTeamsResponse,
): QueryKey {
// First page so just pass empty
if (pageIndex === 0) {
return [QUERY_KEY, ""];
}

// If there's no more data then return an empty next skiptoken
if (previousPageData && ["", null].includes(previousPageData.skip_token)) {
return null;
}

// Next skiptoken exists so pass it
return [QUERY_KEY, previousPageData.skip_token ?? ""];
}
type QueryKey =
| [key: string, tenantId: string, channelId: string, skiptoken: string]
| null;

function useMsTeamsTeams({
queryOptions = {},
Expand All @@ -47,17 +31,61 @@ function useMsTeamsTeams({
const { knockMsTeamsChannelId, tenantId, connectionStatus } =
useKnockMsTeamsClient();

const fetchTeams = (queryKey: QueryKey) =>
knock.msTeams.getTeams({
knockChannelId: knockMsTeamsChannelId,
tenant: tenantId,
queryOptions: {
$skiptoken: queryKey?.[1],
$top: queryOptions?.limitPerPage,
$filter: queryOptions?.filter,
$select: queryOptions?.select,
},
});
// Track previous tenant/channel/connectionStatus to detect changes and clear cache
const prevTenantRef = useRef(tenantId);
const prevChannelRef = useRef(knockMsTeamsChannelId);
const prevConnectionStatusRef = useRef(connectionStatus);

// Create a getQueryKey function that includes tenantId and knockMsTeamsChannelId
// so that SWR treats different tenants as different cache entries
const getQueryKey = useCallback(
(
pageIndex: number,
previousPageData: GetMsTeamsTeamsResponse | null,
): QueryKey => {
// Don't fetch if not connected
if (connectionStatus !== "connected") {
return null;
}

// First page so just pass empty
if (pageIndex === 0) {
return [QUERY_KEY, tenantId, knockMsTeamsChannelId, ""];
}

// If there's no more data then return an empty next skiptoken
if (
previousPageData &&
["", null].includes(previousPageData.skip_token)
) {
return null;
}

// Next skiptoken exists so pass it
return [
QUERY_KEY,
tenantId,
knockMsTeamsChannelId,
previousPageData?.skip_token ?? "",
];
},
[tenantId, knockMsTeamsChannelId, connectionStatus],
);

const fetchTeams = useCallback(
(queryKey: QueryKey) =>
knock.msTeams.getTeams({
knockChannelId: knockMsTeamsChannelId,
tenant: tenantId,
queryOptions: {
$skiptoken: queryKey?.[3],
$top: queryOptions?.limitPerPage,
$filter: queryOptions?.filter,
$select: queryOptions?.select,
},
}),
[knock.msTeams, knockMsTeamsChannelId, tenantId, queryOptions],
);

const { data, error, isLoading, isValidating, setSize, mutate } =
useSWRInfinite<GetMsTeamsTeamsResponse>(getQueryKey, fetchTeams, {
Expand All @@ -66,6 +94,28 @@ function useMsTeamsTeams({
revalidateFirstPage: false,
});

// Clear cache when tenant, channel, or connection status changes
// This ensures that when the user disconnects and reconnects (possibly to a different
// MS Teams workspace), or when the access token is revoked, the cached teams are cleared
useEffect(() => {
const tenantChanged = prevTenantRef.current !== tenantId;
const channelChanged = prevChannelRef.current !== knockMsTeamsChannelId;
// Detect when connection is re-established (was not connected, now is connected)
const wasConnected = prevConnectionStatusRef.current === "connected";
const isConnected = connectionStatus === "connected";
const connectionReestablished = !wasConnected && isConnected;

if (tenantChanged || channelChanged || connectionReestablished) {
// Reset the SWR state to clear cached data
mutate(undefined, { revalidate: false });
setSize(0);
}

prevTenantRef.current = tenantId;
prevChannelRef.current = knockMsTeamsChannelId;
prevConnectionStatusRef.current = connectionStatus;
}, [tenantId, knockMsTeamsChannelId, connectionStatus, mutate, setSize]);

const lastPage = data?.at(-1);
const hasNextPage = lastPage === undefined || !!lastPage.skip_token;

Expand Down
114 changes: 82 additions & 32 deletions packages/react-core/src/modules/slack/hooks/useSlackChannels.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { SlackChannelQueryOptions, useKnockSlackClient } from "..";
import { GetSlackChannelsResponse, SlackChannel } from "@knocklabs/client";
import { useEffect, useMemo } from "react";
import { useCallback, useEffect, useMemo, useRef } from "react";
import useSWRInfinite from "swr/infinite";

import { useKnockClient } from "../../core";
Expand All @@ -21,25 +21,9 @@ type UseSlackChannelOutput = {
refetch: () => void;
};

type QueryKey = [key: string, cursor: string] | null;

function getQueryKey(
pageIndex: number,
previousPageData: GetSlackChannelsResponse,
): QueryKey {
// First page so just pass empty
if (pageIndex === 0) {
return [QUERY_KEY, ""];
}

// If there's no more data then return an empty next cursor
if (previousPageData && ["", null].includes(previousPageData.next_cursor)) {
return null;
}

// Next cursor exists so pass it
return [QUERY_KEY, previousPageData.next_cursor ?? ""];
}
type QueryKey =
| [key: string, tenantId: string, channelId: string, cursor: string]
| null;

function useSlackChannels({
queryOptions,
Expand All @@ -48,25 +32,91 @@ function useSlackChannels({
const { knockSlackChannelId, tenantId, connectionStatus } =
useKnockSlackClient();

const fetchChannels = (queryKey: QueryKey) => {
return knock.slack.getChannels({
tenant: tenantId,
knockChannelId: knockSlackChannelId,
queryOptions: {
...queryOptions,
cursor: queryKey?.[1],
limit: queryOptions?.limitPerPage || LIMIT_PER_PAGE,
types: queryOptions?.types || CHANNEL_TYPES,
},
});
};
// Track previous tenant/channel/connectionStatus to detect changes and clear cache
const prevTenantRef = useRef(tenantId);
const prevChannelRef = useRef(knockSlackChannelId);
const prevConnectionStatusRef = useRef(connectionStatus);

// Create a getQueryKey function that includes tenantId and knockSlackChannelId
// so that SWR treats different tenants as different cache entries
const getQueryKey = useCallback(
(
pageIndex: number,
previousPageData: GetSlackChannelsResponse | null,
): QueryKey => {
// Don't fetch if not connected
if (connectionStatus !== "connected") {
return null;
}

// First page so just pass empty
if (pageIndex === 0) {
return [QUERY_KEY, tenantId, knockSlackChannelId, ""];
}

// If there's no more data then return an empty next cursor
if (
previousPageData &&
["", null].includes(previousPageData.next_cursor)
) {
return null;
}

// Next cursor exists so pass it
return [
QUERY_KEY,
tenantId,
knockSlackChannelId,
previousPageData?.next_cursor ?? "",
];
},
[tenantId, knockSlackChannelId, connectionStatus],
);

const fetchChannels = useCallback(
(queryKey: QueryKey) => {
return knock.slack.getChannels({
tenant: tenantId,
knockChannelId: knockSlackChannelId,
queryOptions: {
...queryOptions,
cursor: queryKey?.[3],
limit: queryOptions?.limitPerPage || LIMIT_PER_PAGE,
types: queryOptions?.types || CHANNEL_TYPES,
},
});
},
[knock.slack, tenantId, knockSlackChannelId, queryOptions],
);

const { data, error, isLoading, isValidating, setSize, mutate } =
useSWRInfinite<GetSlackChannelsResponse>(getQueryKey, fetchChannels, {
initialSize: 0,
revalidateFirstPage: false,
});

// Clear cache when tenant, channel, or connection status changes
// This ensures that when the user disconnects and reconnects (possibly to a different
// Slack workspace), or when the access token is revoked, the cached channels are cleared
useEffect(() => {
const tenantChanged = prevTenantRef.current !== tenantId;
const channelChanged = prevChannelRef.current !== knockSlackChannelId;
// Detect when connection is re-established (was not connected, now is connected)
const wasConnected = prevConnectionStatusRef.current === "connected";
const isConnected = connectionStatus === "connected";
const connectionReestablished = !wasConnected && isConnected;

if (tenantChanged || channelChanged || connectionReestablished) {
// Reset the SWR state to clear cached data
mutate(undefined, { revalidate: false });
setSize(0);
}

prevTenantRef.current = tenantId;
prevChannelRef.current = knockSlackChannelId;
prevConnectionStatusRef.current = connectionStatus;
}, [tenantId, knockSlackChannelId, connectionStatus, mutate, setSize]);

const lastPage = data?.at(-1);
const hasNextPage = lastPage === undefined || !!lastPage.next_cursor;

Expand Down
Loading
Loading