Modernize how we use React contexts (#3359)

* Replace useContext with use

The docs recommend the use hook because it is simpler and allows itself to be called conditionally.

* Simplify our context providers

React 19 lets you omit the '.Provider' bit.
This commit is contained in:
Robin
2025-06-24 04:48:35 -04:00
committed by GitHub
parent a507bcde90
commit 3ffb118dc7
11 changed files with 48 additions and 55 deletions

View File

@@ -77,7 +77,7 @@ export const App: FC<Props> = ({ vm }) => {
{loaded ? ( {loaded ? (
<Suspense fallback={null}> <Suspense fallback={null}>
<ClientProvider> <ClientProvider>
<MediaDevicesContext.Provider value={vm.mediaDevices}> <MediaDevicesContext value={vm.mediaDevices}>
<ProcessorProvider> <ProcessorProvider>
<Sentry.ErrorBoundary <Sentry.ErrorBoundary
fallback={(error) => ( fallback={(error) => (
@@ -96,7 +96,7 @@ export const App: FC<Props> = ({ vm }) => {
</Routes> </Routes>
</Sentry.ErrorBoundary> </Sentry.ErrorBoundary>
</ProcessorProvider> </ProcessorProvider>
</MediaDevicesContext.Provider> </MediaDevicesContext>
</ClientProvider> </ClientProvider>
</Suspense> </Suspense>
) : ( ) : (

View File

@@ -11,7 +11,7 @@ import {
useEffect, useEffect,
useState, useState,
createContext, createContext,
useContext, use,
useRef, useRef,
useMemo, useMemo,
type JSX, type JSX,
@@ -69,8 +69,7 @@ const ClientContext = createContext<ClientState | undefined>(undefined);
export const ClientContextProvider = ClientContext.Provider; export const ClientContextProvider = ClientContext.Provider;
export const useClientState = (): ClientState | undefined => export const useClientState = (): ClientState | undefined => use(ClientContext);
useContext(ClientContext);
export function useClient(): { export function useClient(): {
client?: MatrixClient; client?: MatrixClient;
@@ -350,9 +349,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
return <ErrorPage widget={widget} error={alreadyOpenedErr} />; return <ErrorPage widget={widget} error={alreadyOpenedErr} />;
} }
return ( return <ClientContext value={state}>{children}</ClientContext>;
<ClientContext.Provider value={state}>{children}</ClientContext.Provider>
);
}; };
export type InitResult = { export type InitResult = {

View File

@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details. Please see LICENSE in the repository root for full details.
*/ */
import { createContext, useContext, useMemo } from "react"; import { createContext, use, useMemo } from "react";
import { useObservableEagerState } from "observable-hooks"; import { useObservableEagerState } from "observable-hooks";
import { type MediaDevices } from "./state/MediaDevices"; import { type MediaDevices } from "./state/MediaDevices";
@@ -15,7 +15,7 @@ export const MediaDevicesContext = createContext<MediaDevices | undefined>(
); );
export function useMediaDevices(): MediaDevices { export function useMediaDevices(): MediaDevices {
const mediaDevices = useContext(MediaDevicesContext); const mediaDevices = use(MediaDevicesContext);
if (mediaDevices === undefined) if (mediaDevices === undefined)
throw new Error( throw new Error(
"useMediaDevices must be used within a MediaDevices context provider", "useMediaDevices must be used within a MediaDevices context provider",

View File

@@ -24,7 +24,7 @@ import {
createContext, createContext,
forwardRef, forwardRef,
memo, memo,
useContext, use,
useEffect, useEffect,
useMemo, useMemo,
useRef, useRef,
@@ -124,7 +124,7 @@ interface LayoutContext {
const LayoutContext = createContext<LayoutContext | null>(null); const LayoutContext = createContext<LayoutContext | null>(null);
function useLayoutContext(): LayoutContext { function useLayoutContext(): LayoutContext {
const context = useContext(LayoutContext); const context = use(LayoutContext);
if (context === null) if (context === null)
throw new Error("useUpdateLayout called outside a Grid layout context"); throw new Error("useUpdateLayout called outside a Grid layout context");
return context; return context;
@@ -532,14 +532,14 @@ export function Grid<
className={classNames(className, styles.grid)} className={classNames(className, styles.grid)}
style={style} style={style}
> >
<LayoutContext.Provider value={context}> <LayoutContext value={context}>
<LayoutMemo <LayoutMemo
ref={setLayoutRoot} ref={setLayoutRoot}
Layout={Layout} Layout={Layout}
model={model} model={model}
Slot={Slot} Slot={Slot}
/> />
</LayoutContext.Provider> </LayoutContext>
{tileTransitions((spring, { id, model, onDrag, width, height }) => ( {tileTransitions((spring, { id, model, onDrag, width, height }) => (
<TileWrapper <TileWrapper
key={id} key={id}

View File

@@ -14,7 +14,7 @@ import {
createContext, createContext,
type FC, type FC,
type JSX, type JSX,
useContext, use,
useEffect, useEffect,
useMemo, useMemo,
} from "react"; } from "react";
@@ -34,7 +34,7 @@ type ProcessorState = {
const ProcessorContext = createContext<ProcessorState | undefined>(undefined); const ProcessorContext = createContext<ProcessorState | undefined>(undefined);
export function useTrackProcessor(): ProcessorState { export function useTrackProcessor(): ProcessorState {
const state = useContext(ProcessorContext); const state = use(ProcessorContext);
if (state === undefined) if (state === undefined)
throw new Error( throw new Error(
"useTrackProcessor must be used within a ProcessorProvider", "useTrackProcessor must be used within a ProcessorProvider",
@@ -83,9 +83,5 @@ export const ProcessorProvider: FC<Props> = ({ children }) => {
[supported, blurActivated, blur], [supported, blurActivated, blur],
); );
return ( return <ProcessorContext value={processorState}>{children}</ProcessorContext>;
<ProcessorContext.Provider value={processorState}>
{children}
</ProcessorContext.Provider>
);
}; };

View File

@@ -8,7 +8,7 @@ Please see LICENSE in the repository root for full details.
import { EventType, RelationType } from "matrix-js-sdk"; import { EventType, RelationType } from "matrix-js-sdk";
import { import {
createContext, createContext,
useContext, use,
type ReactNode, type ReactNode,
useCallback, useCallback,
useMemo, useMemo,
@@ -34,7 +34,7 @@ const ReactionsSenderContext = createContext<
>(undefined); >(undefined);
export const useReactionsSender = (): ReactionsSenderContextType => { export const useReactionsSender = (): ReactionsSenderContextType => {
const context = useContext(ReactionsSenderContext); const context = use(ReactionsSenderContext);
if (!context) { if (!context) {
throw new Error("useReactions must be used within a ReactionsProvider"); throw new Error("useReactions must be used within a ReactionsProvider");
} }
@@ -157,7 +157,7 @@ export const ReactionsSenderProvider = ({
); );
return ( return (
<ReactionsSenderContext.Provider <ReactionsSenderContext
value={{ value={{
supportsReactions, supportsReactions,
toggleRaisedHand, toggleRaisedHand,
@@ -165,6 +165,6 @@ export const ReactionsSenderProvider = ({
}} }}
> >
{children} {children}
</ReactionsSenderContext.Provider> </ReactionsSenderContext>
); );
}; };

View File

@@ -149,7 +149,7 @@ function createGroupCallView(
const { getByText } = render( const { getByText } = render(
<BrowserRouter> <BrowserRouter>
<TooltipProvider> <TooltipProvider>
<MediaDevicesContext.Provider value={mockMediaDevices({})}> <MediaDevicesContext value={mockMediaDevices({})}>
<ProcessorProvider> <ProcessorProvider>
<GroupCallView <GroupCallView
client={client} client={client}
@@ -164,7 +164,7 @@ function createGroupCallView(
widget={widget} widget={widget}
/> />
</ProcessorProvider> </ProcessorProvider>
</MediaDevicesContext.Provider> </MediaDevicesContext>
</TooltipProvider> </TooltipProvider>
</BrowserRouter>, </BrowserRouter>,
); );

View File

@@ -149,13 +149,13 @@ function createInCallView(): RenderResult & {
rtcSession.joined = true; rtcSession.joined = true;
const renderResult = render( const renderResult = render(
<BrowserRouter> <BrowserRouter>
<MediaDevicesContext.Provider value={mockMediaDevices({})}> <MediaDevicesContext value={mockMediaDevices({})}>
<ReactionsSenderProvider <ReactionsSenderProvider
vm={vm} vm={vm}
rtcSession={rtcSession as unknown as MatrixRTCSession} rtcSession={rtcSession as unknown as MatrixRTCSession}
> >
<TooltipProvider> <TooltipProvider>
<RoomContext.Provider value={livekitRoom}> <RoomContext value={livekitRoom}>
<InCallView <InCallView
client={client} client={client}
hideHeader={true} hideHeader={true}
@@ -182,10 +182,10 @@ function createInCallView(): RenderResult & {
connState={ConnectionState.Connected} connState={ConnectionState.Connected}
onShareClick={null} onShareClick={null}
/> />
</RoomContext.Provider> </RoomContext>
</TooltipProvider> </TooltipProvider>
</ReactionsSenderProvider> </ReactionsSenderProvider>
</MediaDevicesContext.Provider> </MediaDevicesContext>
</BrowserRouter>, </BrowserRouter>,
); );
return { return {

View File

@@ -172,7 +172,7 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
if (livekitRoom === undefined || vm === null) return null; if (livekitRoom === undefined || vm === null) return null;
return ( return (
<RoomContext.Provider value={livekitRoom}> <RoomContext value={livekitRoom}>
<ReactionsSenderProvider vm={vm} rtcSession={props.rtcSession}> <ReactionsSenderProvider vm={vm} rtcSession={props.rtcSession}>
<InCallView <InCallView
{...props} {...props}
@@ -181,7 +181,7 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
connState={connState} connState={connState}
/> />
</ReactionsSenderProvider> </ReactionsSenderProvider>
</RoomContext.Provider> </RoomContext>
); );
}; };

View File

@@ -124,14 +124,14 @@ describe("useMuteStates", () => {
render( render(
<MemoryRouter> <MemoryRouter>
<MediaDevicesContext.Provider <MediaDevicesContext
value={mockMediaDevices({ value={mockMediaDevices({
microphone: false, microphone: false,
camera: false, camera: false,
})} })}
> >
<TestComponent /> <TestComponent />
</MediaDevicesContext.Provider> </MediaDevicesContext>
</MemoryRouter>, </MemoryRouter>,
); );
expect(screen.getByTestId("audio-enabled").textContent).toBe("false"); expect(screen.getByTestId("audio-enabled").textContent).toBe("false");
@@ -143,9 +143,9 @@ describe("useMuteStates", () => {
render( render(
<MemoryRouter> <MemoryRouter>
<MediaDevicesContext.Provider value={mockMediaDevices()}> <MediaDevicesContext value={mockMediaDevices()}>
<TestComponent /> <TestComponent />
</MediaDevicesContext.Provider> </MediaDevicesContext>
</MemoryRouter>, </MemoryRouter>,
); );
expect(screen.getByTestId("audio-enabled").textContent).toBe("true"); expect(screen.getByTestId("audio-enabled").textContent).toBe("true");
@@ -159,9 +159,9 @@ describe("useMuteStates", () => {
render( render(
<MemoryRouter> <MemoryRouter>
<MediaDevicesContext.Provider value={mockMediaDevices()}> <MediaDevicesContext value={mockMediaDevices()}>
<TestComponent isJoined /> <TestComponent isJoined />
</MediaDevicesContext.Provider> </MediaDevicesContext>
</MemoryRouter>, </MemoryRouter>,
); );
expect(screen.getByTestId("audio-enabled").textContent).toBe("false"); expect(screen.getByTestId("audio-enabled").textContent).toBe("false");
@@ -178,9 +178,9 @@ describe("useMuteStates", () => {
render( render(
<MemoryRouter> <MemoryRouter>
<MediaDevicesContext.Provider value={mockMediaDevices()}> <MediaDevicesContext value={mockMediaDevices()}>
<TestComponent /> <TestComponent />
</MediaDevicesContext.Provider> </MediaDevicesContext>
</MemoryRouter>, </MemoryRouter>,
); );
expect(screen.getByTestId("audio-enabled").textContent).toBe("false"); expect(screen.getByTestId("audio-enabled").textContent).toBe("false");
@@ -192,9 +192,9 @@ describe("useMuteStates", () => {
render( render(
<MemoryRouter initialEntries={["/room/?skipLobby=true"]}> <MemoryRouter initialEntries={["/room/?skipLobby=true"]}>
<MediaDevicesContext.Provider value={mockMediaDevices()}> <MediaDevicesContext value={mockMediaDevices()}>
<TestComponent /> <TestComponent />
</MediaDevicesContext.Provider> </MediaDevicesContext>
</MemoryRouter>, </MemoryRouter>,
); );
expect(screen.getByTestId("audio-enabled").textContent).toBe("false"); expect(screen.getByTestId("audio-enabled").textContent).toBe("false");
@@ -224,13 +224,13 @@ describe("useMuteStates", () => {
return ( return (
<MemoryRouter> <MemoryRouter>
<MediaDevicesContext.Provider value={devices}> <MediaDevicesContext value={devices}>
<TestComponent /> <TestComponent />
<button onClick={onConnectDevicesClick}>Connect devices</button> <button onClick={onConnectDevicesClick}>Connect devices</button>
<button onClick={onDisconnectDevicesClick}> <button onClick={onDisconnectDevicesClick}>
Disconnect devices Disconnect devices
</button> </button>
</MediaDevicesContext.Provider> </MediaDevicesContext>
</MemoryRouter> </MemoryRouter>
); );
}; };

View File

@@ -105,9 +105,9 @@ afterEach(() => {
test("can play a single sound", async () => { test("can play a single sound", async () => {
const { findByText } = render( const { findByText } = render(
<MediaDevicesContext.Provider value={mockMediaDevices({})}> <MediaDevicesContext value={mockMediaDevices({})}>
<TestComponentWrapper /> <TestComponentWrapper />
</MediaDevicesContext.Provider>, </MediaDevicesContext>,
); );
await user.click(await findByText("Valid sound")); await user.click(await findByText("Valid sound"));
expect(testAudioContext.createBufferSource).toHaveBeenCalledOnce(); expect(testAudioContext.createBufferSource).toHaveBeenCalledOnce();
@@ -115,9 +115,9 @@ test("can play a single sound", async () => {
test("will ignore sounds that are not registered", async () => { test("will ignore sounds that are not registered", async () => {
const { findByText } = render( const { findByText } = render(
<MediaDevicesContext.Provider value={mockMediaDevices({})}> <MediaDevicesContext value={mockMediaDevices({})}>
<TestComponentWrapper /> <TestComponentWrapper />
</MediaDevicesContext.Provider>, </MediaDevicesContext>,
); );
await user.click(await findByText("Invalid sound")); await user.click(await findByText("Invalid sound"));
expect(testAudioContext.createBufferSource).not.toHaveBeenCalled(); expect(testAudioContext.createBufferSource).not.toHaveBeenCalled();
@@ -125,7 +125,7 @@ test("will ignore sounds that are not registered", async () => {
test("will use the correct device", () => { test("will use the correct device", () => {
render( render(
<MediaDevicesContext.Provider <MediaDevicesContext
value={mockMediaDevices({ value={mockMediaDevices({
audioOutput: { audioOutput: {
available$: of(new Map<never, never>()), available$: of(new Map<never, never>()),
@@ -135,7 +135,7 @@ test("will use the correct device", () => {
})} })}
> >
<TestComponentWrapper /> <TestComponentWrapper />
</MediaDevicesContext.Provider>, </MediaDevicesContext>,
); );
expect(testAudioContext.createBufferSource).not.toHaveBeenCalled(); expect(testAudioContext.createBufferSource).not.toHaveBeenCalled();
expect(testAudioContext.setSinkId).toHaveBeenCalledWith("chosen-device"); expect(testAudioContext.setSinkId).toHaveBeenCalledWith("chosen-device");
@@ -144,9 +144,9 @@ test("will use the correct device", () => {
test("will use the correct volume level", async () => { test("will use the correct volume level", async () => {
soundEffectVolumeSetting.setValue(0.33); soundEffectVolumeSetting.setValue(0.33);
const { findByText } = render( const { findByText } = render(
<MediaDevicesContext.Provider value={mockMediaDevices({})}> <MediaDevicesContext value={mockMediaDevices({})}>
<TestComponentWrapper /> <TestComponentWrapper />
</MediaDevicesContext.Provider>, </MediaDevicesContext>,
); );
await user.click(await findByText("Valid sound")); await user.click(await findByText("Valid sound"));
expect(testAudioContext.gain.gain.setValueAtTime).toHaveBeenCalledWith( expect(testAudioContext.gain.gain.setValueAtTime).toHaveBeenCalledWith(
@@ -158,7 +158,7 @@ test("will use the correct volume level", async () => {
test("will use the pan if earpiece is selected", async () => { test("will use the pan if earpiece is selected", async () => {
const { findByText } = render( const { findByText } = render(
<MediaDevicesContext.Provider <MediaDevicesContext
value={mockMediaDevices({ value={mockMediaDevices({
audioOutput: { audioOutput: {
available$: of(new Map<never, never>()), available$: of(new Map<never, never>()),
@@ -168,7 +168,7 @@ test("will use the pan if earpiece is selected", async () => {
})} })}
> >
<TestComponentWrapper /> <TestComponentWrapper />
</MediaDevicesContext.Provider>, </MediaDevicesContext>,
); );
await user.click(await findByText("Valid sound")); await user.click(await findByText("Valid sound"));
expect(testAudioContext.pan.pan.setValueAtTime).toHaveBeenCalledWith(1, 0); expect(testAudioContext.pan.pan.setValueAtTime).toHaveBeenCalledWith(1, 0);