Channel list rendered two same channels

[Problem/Question]
Hey there!
I implement Sendbird with this example, but I have a bug. I have two channels first private channel and a second group. Members of the private channel it is users ‘ID1’, and ‘ID2’ and a second it is ‘ID1’, ‘ID2’, and ‘ID3’. Both channels were created with params isDistinct: true.
Sometimes when I move from one channel to another, my channel list rendered the same channel and I got two of the same channel in the channel list and a third where I move. I figured out that it happening when user got messages. I think that something is wrong with the ‘handleJoinChannel’ function.
Could you help me with it, please?

Video of reproducing - link

My general component:

import React, { useState, useEffect, useRef } from 'react';
import SendbirdChat from '@sendbird/chat';
import {
  GroupChannelModule,
  GroupChannelFilter,
  GroupChannelListOrder,
  MessageFilter,
  MessageCollectionInitPolicy,
} from '@sendbird/chat/groupChannel';

import css from './TripChat.module.css';

import { useSelector } from 'react-redux';
import { getCurrentUser } from 'ducks/user.duck';
import { ChatTypes } from 'lib/enums';
import ChannelList from 'containers/Chat/components/ChannelList/ChannelList';
import { injectIntl } from 'react-intl/dist/react-intl';
import Channel from 'containers/Chat/components/Channel/Channel';
import MessagesList from 'containers/Chat/components/MessagesList/MessagesList';
import MessageInput from 'containers/Chat/components/MessageInput/MessageInput';
import { useParams } from 'react-router-dom';
import useSendbirdApp from 'lib/util/useSendbirdApp';

let sb = null;

const TripChatComponent = props => {
  const [state, updateState] = useState({
    applicationUsers: [],
    groupChannelMembers: [],
    currentlyJoinedChannel: null,
    messages: [],
    channels: [],
    messageInputValue: '',
    userNameInputValue: '',
    userIdInputValue: '',
    channelNameUpdateValue: '',
    settingUpUser: true,
    file: null,
    messageToUpdate: null,
    messageCollection: null,
    loading: false,
    error: false,
    isReactions: false,
    currentMessage: {},
  });

  const { intl } = props;

  const { id } = useParams();

  const stateRef = useRef();
  stateRef.current = state;

  const channelRef = useRef();

  const currentUser = useSelector(getCurrentUser);

  useEffect(() => {
    const setupUser = async userId => {
      const sendbirdChat = await useSendbirdApp();

      if (sendbirdChat) {
        await sendbirdChat.connect(userId);
        await sendbirdChat.setChannelInvitationPreference(true);

        sb = sendbirdChat;
        updateState({ ...state, loading: true });
        const [channels, error] = await loadChannels(channelHandlers);
        if (error) {
          return onError(error);
        }

        updateState({
          ...state,
          channels: channels,
          loading: false,
          settingUpUser: false,
        });
      }
    };

    if (currentUser) {
      const userId = currentUser?.id?.uuid;
      setupUser(userId);
    }
  }, []);

  useEffect(() => {
    if (state.channels && id) {
      const currentChannel = state.channels.find(channel => channel.url === id);

      if (currentChannel && !state.currentlyJoinedChannel) {
        handleJoinChannel(currentChannel.url);
      }
    }
  }, [state.channels]);

  const channelHandlers = {
    onChannelsAdded: (context, channels) => {
      const updatedChannels = [...channels, ...stateRef.current.channels];
      updateState({ ...stateRef.current, channels: updatedChannels, applicationUsers: [] });
    },
    onChannelsDeleted: (context, channels) => {
      const updatedChannels = stateRef.current.channels.filter(channel => {
        return !channels.includes(channel.url);
      });
      updateState({ ...stateRef.current, channels: updatedChannels });
    },
    onChannelsUpdated: (context, channels) => {
      const updatedChannels = stateRef.current.channels.map(channel => {
        const updatedChannel = channels.find(
          incomingChannel => incomingChannel.url === channel.url
        );
        if (updatedChannel) {
          return updatedChannel;
        } else {
          return channel;
        }
      });

      updateState({ ...stateRef.current, channels: updatedChannels });
    },
  };

  const messageHandlers = {
    onMessagesAdded: (context, channel, messages) => {
      const updatedMessages = [...stateRef.current.messages, ...messages];

      updateState({ ...stateRef.current, messages: updatedMessages });
    },
    onMessagesUpdated: (context, channel, messages) => {
      const updatedMessages = [...stateRef.current.messages];
      for (let i in messages) {
        const incomingMessage = messages[i];
        const indexOfExisting = stateRef.current.messages.findIndex(message => {
          return incomingMessage.reqId === message.reqId;
        });

        if (indexOfExisting !== -1) {
          updatedMessages[indexOfExisting] = incomingMessage;
        }
        if (!incomingMessage.reqId) {
          updatedMessages.push(incomingMessage);
        }
      }

      updateState({ ...stateRef.current, messages: updatedMessages });
    },
    onMessagesDeleted: (context, channel, messageIds) => {
      const updateMessages = stateRef.current.messages.filter(message => {
        return !messageIds.includes(message.messageId);
      });
      updateState({ ...stateRef.current, messages: updateMessages });
    },
    onChannelUpdated: (context, channel) => {},
    onChannelDeleted: (context, channelUrl) => {},
    onHugeGapDetected: () => {},
  };

  const scrollToBottom = (item, smooth) => {
    item?.scrollTo({
      top: item.scrollHeight,
      behavior: smooth,
    });
  };

  useEffect(() => {
    scrollToBottom(channelRef.current, 'smooth');
  }, [state.currentlyJoinedChannel]);

  useEffect(() => {
    scrollToBottom(channelRef.current, 'smooth');
  }, [state.messages]);

  const onError = error => {
    updateState({ ...state, error: error.message });
    console.log(error, ' - THIS ERROR');
  };

  const handleJoinChannel = async channelUrl => {
    if (state.messageCollection && state.messageCollection.dispose) {
      state.messageCollection?.dispose();
    }

    if (state.currentlyJoinedChannel?.url === channelUrl) {
      return null;
    }
    const { channels } = state;
    updateState({ ...state, loading: true });
    const channel = channels.find(channel => channel.url === channelUrl);

    const onCacheResult = (err, messages) => {
      updateState({
        ...stateRef.current,
        currentlyJoinedChannel: channel,
        messages: messages.reverse(),
        loading: false,
      });
    };

    const onApiResult = (err, messages) => {
      updateState({
        ...stateRef.current,
        currentlyJoinedChannel: channel,
        messages: messages.reverse(),
        loading: false,
      });
    };

    const collection = loadMessages(channel, messageHandlers, onCacheResult, onApiResult);

    updateState({ ...state, messageCollection: collection });
  };

  const handleLeaveChannel = async () => {
    const { currentlyJoinedChannel } = state;
    await currentlyJoinedChannel.leave();

    updateState({ ...state, currentlyJoinedChannel: null });
  };

  const handleCreateChannel = async (channelName = 'testChannel') => {
    const [groupChannel, error] = await createChannel(channelName, state.groupChannelMembers);
    if (error) {
      return onError(error);
    }
  };

  const handleUpdateChannelMembersList = async () => {
    const { currentlyJoinedChannel, groupChannelMembers } = state;
    await inviteUsersToChannel(currentlyJoinedChannel, groupChannelMembers);
    updateState({ ...state, applicationUsers: [] });
  };

  const onMessageInputChange = e => {
    const messageInputValue = e.currentTarget.value;
    updateState({ ...state, messageInputValue });
  };

  const sendMessage = async () => {
    const { messageToUpdate, currentlyJoinedChannel, messages } = state;
    if (messageToUpdate) {
      const userMessageUpdateParams = {};
      userMessageUpdateParams.message = state.messageInputValue;
      const updatedMessage = await currentlyJoinedChannel.updateUserMessage(
        messageToUpdate.messageId,
        userMessageUpdateParams
      );
      const messageIndex = messages.findIndex(item => item.messageId == messageToUpdate.messageId);
      messages[messageIndex] = updatedMessage;
      updateState({ ...state, messages: messages, messageInputValue: '', messageToUpdate: null });
    } else {
      const userMessageParams = {};
      userMessageParams.message = state.messageInputValue;
      currentlyJoinedChannel
        .sendUserMessage(userMessageParams)
        .onSucceeded(message => {
          updateState({ ...stateRef.current, messageInputValue: '' });
        })
        .onFailed(error => {
          console.log(error);
          console.log('failed');
        });
    }
  };

  const onFileInputChange = async e => {
    if (e.currentTarget.files && e.currentTarget.files.length > 0) {
      const { currentlyJoinedChannel, messages } = state;
      const fileMessageParams = {};
      fileMessageParams.file = e.currentTarget.files[0];
      currentlyJoinedChannel
        .sendFileMessage(fileMessageParams)
        .onSucceeded(message => {
          updateState({ ...stateRef.current, messageInputValue: '', file: null });
        })
        .onFailed(error => {
          console.log(error);
          console.log('failed');
        });
    }
  };

  const handleDeleteMessage = async messageToDelete => {
    const { currentlyJoinedChannel } = state;
    await deleteMessage(currentlyJoinedChannel, messageToDelete); // Delete
  };

  const updateMessage = async message => {
    updateState({ ...state, messageToUpdate: message, messageInputValue: message.message });
  };

  const updateMessageReactions = async message => {
    const { messages, currentlyJoinedChannel } = state;

    const userMessageUpdateParams = {};
    const updatedMessage = await currentlyJoinedChannel.updateUserMessage(
      message.messageId,
      userMessageUpdateParams
    );
    const messageIndex = messages.findIndex(item => item.messageId === message.messageId);
    messages[messageIndex] = updatedMessage;

    updateState({ ...state, messages: messages, isReactions: false });
  };

  const toggleReactions = async message => {
    const { isReactions } = state;
    updateState({ ...state, isReactions: !isReactions, currentMessage: message });
  };

  const addMessageReaction = async (message, e) => {
    const { currentlyJoinedChannel } = state;

    const emojiKey = e.target.innerText;
    const reactionEvent = await currentlyJoinedChannel.addReaction(message, emojiKey);
    message.applyReactionEvent(reactionEvent);

    updateMessageReactions(message);

    updateState({ ...state, isReactions: false, currentMessage: {} });
  };

  const removeMessageReaction = async (message, messageKey) => {
    const { currentlyJoinedChannel } = state;
    const reactionEvent = await currentlyJoinedChannel.deleteReaction(message, messageKey);
    message.applyReactionEvent(reactionEvent);

    updateMessageReactions(message);
  };

  const handleLoadMemberSelectionList = async () => {
    updateState({ ...state, currentlyJoinedChannel: null });
    const [users, error] = await getAllApplicationUsers();
    if (error) {
      return onError(error);
    }
    updateState({
      ...state,
      currentlyJoinedChannel: null,
      applicationUsers: users,
      groupChannelMembers: [sb.currentUser.userId],
    });
  };

  const handleDeleteChannel = async channelUrl => {
    const [channel, error] = await deleteChannel(channelUrl);
    updateState({ ...state, currentlyJoinedChannel: null });
    if (error) {
      return onError(error);
    }
  };

  const addToChannelMembersList = userId => {
    const groupChannelMembers = [...state.groupChannelMembers, userId];
    updateState({ ...state, groupChannelMembers: groupChannelMembers });
  };

  if (state.error) {
    return <div className="error">{state.error} check console for more information.</div>;
  }

  return (
    <div className={css.chatWrapper}>
      <div className={css.channelListsWrapper}>
        <ChannelList
          channels={state.channels}
          handleJoinChannel={handleJoinChannel}
          intl={intl}
          type={ChatTypes.GROUP_CHAT}
          handleDeleteChannel={handleDeleteChannel}
          currentlyJoinedChannel={state.currentlyJoinedChannel}
          loading={state.loading}
        />
        <ChannelList
          channels={state.channels}
          handleJoinChannel={handleJoinChannel}
          handleDeleteChannel={handleDeleteChannel}
          intl={intl}
          type={ChatTypes.PRIVATE_CHAT}
          currentlyJoinedChannel={state.currentlyJoinedChannel}
          loading={state.loading}
        />
      </div>
      <Channel
        currentlyJoinedChannel={state.currentlyJoinedChannel}
        handleLeaveChannel={handleLeaveChannel}
      >
        <MessagesList
          messages={state.messages}
          handleDeleteMessage={handleDeleteMessage}
          updateMessage={updateMessage}
          addMessageReaction={addMessageReaction}
          removeMessageReaction={removeMessageReaction}
          toggleReactions={toggleReactions}
          isReactions={state.isReactions}
          currentMessage={state.currentMessage}
          sb={sb}
          channelRef={channelRef}
          intl={intl}
        />
        <MessageInput
          value={state.messageInputValue}
          onChange={onMessageInputChange}
          sendMessage={sendMessage}
          fileSelected={state.file}
          onFileInputChange={onFileInputChange}
        />
      </Channel>
    </div>
  );
};

const loadChannels = async channelHandlers => {
  const groupChannelFilter = new GroupChannelFilter();
  groupChannelFilter.includeEmpty = true;

  const collection = sb.groupChannel.createGroupChannelCollection({
    filter: groupChannelFilter,
    order: GroupChannelListOrder.LATEST_LAST_MESSAGE,
  });

  collection.setGroupChannelCollectionHandler(channelHandlers);

  const channels = await collection.loadMore();
  return [channels, null];
};

const loadMessages = (channel, messageHandlers, onCacheResult, onApiResult) => {
  const messageFilter = new MessageFilter();

  const collection = channel.createMessageCollection({
    filter: messageFilter,
    startingPoint: Date.now(),
    limit: 100,
  });

  collection.setMessageCollectionHandler(messageHandlers);
  collection
    .initialize(MessageCollectionInitPolicy.CACHE_AND_REPLACE_BY_API)
    .onCacheResult(onCacheResult)
    .onApiResult(onApiResult);
  return collection;
};

const inviteUsersToChannel = async (channel, userIds) => {
  await channel.inviteWithUserIds(userIds);
};

const createChannel = async (channelName, userIdsToInvite) => {
  try {
    const groupChannelParams = {};
    groupChannelParams.invitedUserIds = userIdsToInvite;
    groupChannelParams.name = channelName;
    groupChannelParams.operatorUserIds = userIdsToInvite;
    const groupChannel = await sb.groupChannel.createChannel(groupChannelParams);
    return [groupChannel, null];
  } catch (error) {
    return [null, error];
  }
};

const deleteChannel = async channelUrl => {
  try {
    const channel = await sb.groupChannel.getChannel(channelUrl);
    await channel.delete();
    return [channel, null];
  } catch (error) {
    return [null, error];
  }
};

const deleteMessage = async (currentlyJoinedChannel, messageToDelete) => {
  await currentlyJoinedChannel.deleteMessage(messageToDelete);
};

const getAllApplicationUsers = async () => {
  try {
    const userQuery = sb.createApplicationUserListQuery({ limit: 100 });
    const users = await userQuery.next();
    return [users, null];
  } catch (error) {
    return [null, error];
  }
};

const TripChat = injectIntl(TripChatComponent);

export default TripChat;


[SDK Version]
sendbird/chat": “^4.8.1”

Hello @NikShein,

The handleJoinChannel doesn’t directly impact the channels portion of the state which would cause the ChannelList component to render the channel twice. The issue must exist outside of that function. Even though you originally used our sample as a guide, it’s been altered enough that it’s not really related to our sample anymore.

This issue isn’t reproducible in the linked sample application and is ultimately more of a React/JS problem than a Sendbird issue since Sendbird doesn’t directly control the state.