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/CallProvider.tsx b/client/src/contexts/CallProvider.tsx
index b20d0e7..8be6dff 100644
--- a/client/src/contexts/CallProvider.tsx
+++ b/client/src/contexts/CallProvider.tsx
@@ -16,15 +16,15 @@
* <https://www.gnu.org/licenses/>.
*/
import { CallAction, CallBegin, WebSocketMessageType } from 'jami-web-common';
-import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
-import { Navigate } from 'react-router-dom';
+import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
-import LoadingPage from '../components/Loading';
+import { createOptionalContext } from '../hooks/createOptionalContext';
import { Conversation } from '../models/conversation';
import { callTimeoutMs } from '../utils/constants';
import { AsyncSetState, SetState, WithChildren } from '../utils/utils';
-import { CallManagerContext } from './CallManagerProvider';
-import { MediaDevicesInfo, MediaInputKind, WebRtcContext } from './WebRtcProvider';
+import { CallData, CallManagerContext } from './CallManagerProvider';
+import ConditionalContextProvider from './ConditionalContextProvider';
+import { IWebRtcContext, MediaDevicesInfo, MediaInputKind, useWebRtcContext } from './WebRtcProvider';
import { IWebSocketContext, WebSocketContext } from './WebSocketProvider';
export type CallRole = 'caller' | 'receiver';
@@ -70,71 +70,53 @@
endCall: () => void;
}
-const defaultCallContext: ICallContext = {
- mediaDevices: {
- audioinput: [],
- audiooutput: [],
- videoinput: [],
- },
- currentMediaDeviceIds: {
- audioinput: {
- id: undefined,
- setId: async () => {},
- },
- audiooutput: {
- id: undefined,
- setId: async () => {},
- },
- videoinput: {
- id: undefined,
- setId: async () => {},
- },
- },
-
- isAudioOn: false,
- setIsAudioOn: () => {},
- videoStatus: VideoStatus.Off,
- updateVideoStatus: () => Promise.reject(),
- isChatShown: false,
- setIsChatShown: () => {},
- isFullscreen: false,
- setIsFullscreen: () => {},
- callRole: 'caller',
- callStatus: CallStatus.Default,
- callStartTime: undefined,
-
- acceptCall: (_: boolean) => {},
- endCall: () => {},
-};
-
-export const CallContext = createContext<ICallContext>(defaultCallContext);
+const optionalCallContext = createOptionalContext<ICallContext>('CallContext');
+export const useCallContext = optionalCallContext.useOptionalContext;
export default ({ children }: WithChildren) => {
const webSocket = useContext(WebSocketContext);
- const { callConversation, callData } = useContext(CallManagerContext);
+ const { callConversation, callData, exitCall } = useContext(CallManagerContext);
+ const webRtcContext = useWebRtcContext(true);
- if (!webSocket || !callConversation || !callData?.conversationId) {
- return <LoadingPage />;
- }
+ const dependencies = useMemo(
+ () => ({
+ webSocket,
+ webRtcContext,
+ callConversation,
+ callData,
+ exitCall,
+ conversationId: callData?.conversationId,
+ }),
+ [webSocket, webRtcContext, callConversation, callData, exitCall]
+ );
return (
- <CallProvider webSocket={webSocket} conversation={callConversation} conversationId={callData?.conversationId}>
+ <ConditionalContextProvider
+ Context={optionalCallContext.Context}
+ initialValue={undefined}
+ dependencies={dependencies}
+ useProviderValue={CallProvider}
+ >
{children}
- </CallProvider>
+ </ConditionalContextProvider>
);
};
const CallProvider = ({
- children,
- conversation,
+ webRtcContext,
+ callConversation,
+ callData,
+ exitCall,
conversationId,
webSocket,
-}: WithChildren & {
+}: {
webSocket: IWebSocketContext;
- conversation: Conversation;
+ webRtcContext: IWebRtcContext;
+ callConversation: Conversation;
+ callData: CallData;
+ exitCall: () => void;
conversationId: string;
-}) => {
- const { callData, exitCall } = useContext(CallManagerContext);
+}): ICallContext => {
const {
localStream,
updateScreenShare,
@@ -143,9 +125,13 @@
closeConnection,
getMediaDevices,
updateLocalStream,
- } = useContext(WebRtcContext);
+ } = webRtcContext;
- const [mediaDevices, setMediaDevices] = useState(defaultCallContext.mediaDevices);
+ const [mediaDevices, setMediaDevices] = useState<MediaDevicesInfo>({
+ audioinput: [],
+ audiooutput: [],
+ videoinput: [],
+ });
const [audioInputDeviceId, setAudioInputDeviceId] = useState<string>();
const [audioOutputDeviceId, setAudioOutputDeviceId] = useState<string>();
const [videoDeviceId, setVideoDeviceId] = useState<string>();
@@ -161,7 +147,7 @@
// TODO: This logic will have to change to support multiple people in a call. Could we move this logic to the server?
// The client could make a single request with the conversationId, and the server would be tasked with sending
// all the individual requests to the members of the conversation.
- const contactUri = useMemo(() => conversation.getFirstMember().contact.uri, [conversation]);
+ const contactUri = useMemo(() => callConversation.getFirstMember().contact.uri, [callConversation]);
useEffect(() => {
if (callStatus !== CallStatus.InCall) {
@@ -406,32 +392,40 @@
};
}, [updateLocalStream, audioInputDeviceId, audioOutputDeviceId, videoDeviceId]);
- if (!callData || !callRole) {
- console.error('Invalid route. Redirecting...');
- return <Navigate to={'/'} />;
- }
-
- return (
- <CallContext.Provider
- value={{
- mediaDevices,
- currentMediaDeviceIds,
- isAudioOn,
- setIsAudioOn,
- videoStatus,
- updateVideoStatus,
- isChatShown,
- setIsChatShown,
- isFullscreen,
- setIsFullscreen,
- callRole,
- callStatus,
- callStartTime,
- acceptCall,
- endCall,
- }}
- >
- {children}
- </CallContext.Provider>
+ return useMemo(
+ () => ({
+ mediaDevices,
+ currentMediaDeviceIds,
+ isAudioOn,
+ setIsAudioOn,
+ videoStatus,
+ updateVideoStatus,
+ isChatShown,
+ setIsChatShown,
+ isFullscreen,
+ setIsFullscreen,
+ callRole,
+ callStatus,
+ callStartTime,
+ acceptCall,
+ endCall,
+ }),
+ [
+ mediaDevices,
+ currentMediaDeviceIds,
+ isAudioOn,
+ setIsAudioOn,
+ videoStatus,
+ updateVideoStatus,
+ isChatShown,
+ setIsChatShown,
+ isFullscreen,
+ setIsFullscreen,
+ callRole,
+ callStatus,
+ callStartTime,
+ acceptCall,
+ endCall,
+ ]
);
};