Fix components rerendering unnecessarily

Before, some providers (WebRtcProvider, CallProvider...) were rendered conditionally.
This was causing all their children to re-render when the provider rendered.

Instead of using conditional rendering, these providers now use `ConditionalContextProvider`.
It always renders the provider, but sets its value to `initialValue` if the dependencies are not all defined.
If all dependencies are defined, the value is the result of the `useProviderValue` hook.

Changes:
- New file: `ConditionalContextProvider`
- New file: `HookComponent` - Component that calls a hook when mounted and calls a `callback` function with the hook result as argument
- For `WebRtcProvider` and `CallProvider`, use `createOptionalContext` to create the context and `ConditionalContextProvider` to provide their value only when the conditions are met
- In `WebRtcProvider`, set the webRtcConnection to undefined when not in a call
- For all providers, wrap their `value` prop in `useMemo` to avoid unnecessary rerenders

Change-Id: Ide947e216d54599aabc71cf4bd026bd20d6e0daf
diff --git a/client/src/contexts/WebRtcProvider.tsx b/client/src/contexts/WebRtcProvider.tsx
index bb99717..55a3a1d 100644
--- a/client/src/contexts/WebRtcProvider.tsx
+++ b/client/src/contexts/WebRtcProvider.tsx
@@ -17,20 +17,21 @@
  */
 
 import { WebRtcIceCandidate, WebRtcSdp, WebSocketMessageType } from 'jami-web-common';
-import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
+import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
 
-import LoadingPage from '../components/Loading';
+import { createOptionalContext } from '../hooks/createOptionalContext';
 import { Conversation } from '../models/conversation';
 import { WithChildren } from '../utils/utils';
 import { useAuthContext } from './AuthProvider';
 import { CallManagerContext } from './CallManagerProvider';
+import ConditionalContextProvider from './ConditionalContextProvider';
 import { IWebSocketContext, WebSocketContext } from './WebSocketProvider';
 
 export type MediaDevicesInfo = Record<MediaDeviceKind, MediaDeviceInfo[]>;
 export type MediaInputKind = 'audio' | 'video';
 export type MediaInputIds = Record<MediaInputKind, string | false | undefined>;
 
-interface IWebRtcContext {
+export interface IWebRtcContext {
   iceConnectionState: RTCIceConnectionState | undefined;
 
   localStream: MediaStream | undefined;
@@ -44,19 +45,8 @@
   closeConnection: () => void;
 }
 
-const defaultWebRtcContext: IWebRtcContext = {
-  iceConnectionState: undefined,
-  localStream: undefined,
-  screenShareLocalStream: undefined,
-  remoteStreams: undefined,
-  getMediaDevices: async () => Promise.reject(),
-  updateLocalStream: async () => Promise.reject(),
-  updateScreenShare: async () => Promise.reject(),
-  sendWebRtcOffer: async () => Promise.reject(),
-  closeConnection: () => {},
-};
-
-export const WebRtcContext = createContext<IWebRtcContext>(defaultWebRtcContext);
+const optionalWebRtcContext = createOptionalContext<IWebRtcContext>('WebRtcContext');
+export const useWebRtcContext = optionalWebRtcContext.useOptionalContext;
 
 export default ({ children }: WithChildren) => {
   const { account } = useAuthContext();
@@ -65,7 +55,12 @@
   const { callConversation, callData } = useContext(CallManagerContext);
 
   useEffect(() => {
-    if (!webRtcConnection && account) {
+    if (webRtcConnection && !callData) {
+      setWebRtcConnection(undefined);
+      return;
+    }
+
+    if (!webRtcConnection && account && callData) {
       const iceServers: RTCIceServer[] = [];
 
       if (account.details['TURN.enable'] === 'true') {
@@ -84,31 +79,36 @@
 
       setWebRtcConnection(new RTCPeerConnection({ iceServers }));
     }
-  }, [account, webRtcConnection]);
+  }, [account, webRtcConnection, callData]);
 
-  if (!webRtcConnection || !webSocket || !callConversation || !callData?.conversationId) {
-    return <LoadingPage />;
-  }
+  const dependencies = useMemo(
+    () => ({
+      webRtcConnection,
+      webSocket,
+      conversation: callConversation,
+      conversationId: callData?.conversationId,
+    }),
+    [webRtcConnection, webSocket, callConversation, callData?.conversationId]
+  );
 
   return (
-    <WebRtcProvider
-      webRtcConnection={webRtcConnection}
-      webSocket={webSocket}
-      conversation={callConversation}
-      conversationId={callData.conversationId}
+    <ConditionalContextProvider
+      Context={optionalWebRtcContext.Context}
+      initialValue={undefined}
+      dependencies={dependencies}
+      useProviderValue={useWebRtcContextValue}
     >
       {children}
-    </WebRtcProvider>
+    </ConditionalContextProvider>
   );
 };
 
-const WebRtcProvider = ({
-  children,
+const useWebRtcContextValue = ({
   conversation,
   conversationId,
   webRtcConnection,
   webSocket,
-}: WithChildren & {
+}: {
   webRtcConnection: RTCPeerConnection;
   webSocket: IWebSocketContext;
   conversation: Conversation;
@@ -438,21 +438,28 @@
     webRtcConnection.close();
   }, [webRtcConnection, localStream, screenShareLocalStream]);
 
-  return (
-    <WebRtcContext.Provider
-      value={{
-        iceConnectionState,
-        localStream,
-        screenShareLocalStream,
-        remoteStreams,
-        getMediaDevices,
-        updateLocalStream,
-        updateScreenShare,
-        sendWebRtcOffer,
-        closeConnection,
-      }}
-    >
-      {children}
-    </WebRtcContext.Provider>
+  return useMemo(
+    () => ({
+      iceConnectionState,
+      localStream,
+      screenShareLocalStream,
+      remoteStreams,
+      getMediaDevices,
+      updateLocalStream,
+      updateScreenShare,
+      sendWebRtcOffer,
+      closeConnection,
+    }),
+    [
+      iceConnectionState,
+      localStream,
+      screenShareLocalStream,
+      remoteStreams,
+      getMediaDevices,
+      updateLocalStream,
+      updateScreenShare,
+      sendWebRtcOffer,
+      closeConnection,
+    ]
   );
 };