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/CallManagerProvider.tsx b/client/src/contexts/CallManagerProvider.tsx
index 9bde186..7d8d843 100644
--- a/client/src/contexts/CallManagerProvider.tsx
+++ b/client/src/contexts/CallManagerProvider.tsx
@@ -16,7 +16,7 @@
  * <https://www.gnu.org/licenses/>.
  */
 import { CallBegin, WebSocketMessageType } from 'jami-web-common';
-import { createContext, useCallback, useContext, useEffect, useState } from 'react';
+import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
 import { useNavigate } from 'react-router-dom';
 
 import { RemoteVideoOverlay } from '../components/VideoOverlay';
@@ -29,7 +29,7 @@
 import WebRtcProvider from './WebRtcProvider';
 import { WebSocketContext } from './WebSocketProvider';
 
-type CallData = {
+export type CallData = {
   conversationId: string;
   role: CallRole;
   withVideoOn?: boolean;
@@ -43,15 +43,23 @@
   exitCall: () => void;
 };
 
-export const CallManagerContext = createContext<ICallManagerContext>(undefined!);
+const defaultCallManagerContext: ICallManagerContext = {
+  callData: undefined,
+  callConversation: undefined,
+
+  startCall: () => {},
+  exitCall: () => {},
+};
+
+export const CallManagerContext = createContext<ICallManagerContext>(defaultCallManagerContext);
 CallManagerContext.displayName = 'CallManagerContext';
 
 export default ({ children }: WithChildren) => {
   const [callData, setCallData] = useState<CallData>();
   const webSocket = useContext(WebSocketContext);
   const navigate = useNavigate();
-  const conversationId = callData?.conversationId;
-  const { conversation } = useConversationQuery(conversationId);
+  const { conversation } = useConversationQuery(callData?.conversationId);
+  const { urlParams } = useUrlParams<ConversationRouteParams>();
 
   const failStartCall = useCallback(() => {
     throw new Error('Cannot start call: Already in a call');
@@ -90,37 +98,26 @@
     };
   }, [webSocket, navigate, startCall, callData]);
 
-  return (
-    <CallManagerContext.Provider
-      value={{
-        startCall,
-        callData,
-        callConversation: conversation,
-        exitCall,
-      }}
-    >
-      <CallManagerProvider>{children}</CallManagerProvider>
-    </CallManagerContext.Provider>
+  const value = useMemo(
+    () => ({
+      startCall,
+      callData,
+      callConversation: conversation,
+      exitCall,
+    }),
+    [startCall, callData, conversation, exitCall]
   );
-};
-
-const CallManagerProvider = ({ children }: WithChildren) => {
-  const { callData } = useContext(CallManagerContext);
-  const { urlParams } = useUrlParams<ConversationRouteParams>();
-  const conversationId = urlParams.conversationId;
-
-  if (!callData) {
-    return <>{children}</>;
-  }
 
   return (
-    <WebRtcProvider>
-      <CallProvider>
-        {callData.conversationId !== conversationId && (
-          <RemoteVideoOverlay callConversationId={callData.conversationId} />
-        )}
-        {children}
-      </CallProvider>
-    </WebRtcProvider>
+    <CallManagerContext.Provider value={value}>
+      <WebRtcProvider>
+        <CallProvider>
+          {callData && callData.conversationId !== urlParams.conversationId && (
+            <RemoteVideoOverlay callConversationId={callData.conversationId} />
+          )}
+          {children}
+        </CallProvider>
+      </WebRtcProvider>
+    </CallManagerContext.Provider>
   );
 };