mirror of
https://github.com/vector-im/element-call.git
synced 2026-01-30 03:15:55 +00:00
* Avoid reactivity bugs in how we track external state
Many of our hooks which attempt to bridge external state from an EventEmitter or EventTarget into React had subtle bugs which could cause them to fail to react to certain updates. The conditions necessary for triggering these bugs are explained by the tests that I've included.
In the majority of cases, I don't think we were triggering these bugs in practice. They could've become problems if we refactored our components in certain ways. The one concrete case I'm aware of in which we actually triggered such a bug was the race condition with the useRoomEncryptionSystem shared secret logic (addressed by a1110af6d5).
But, particularly with all the weird reactivity issues we're debugging this week, I think we need to eliminate the possibility that any of the bugs in these hooks are the cause of our current headaches.
* Reuse useTypedEventEmitterState in useLocalStorage
* Fix type error
89 lines
2.4 KiB
TypeScript
89 lines
2.4 KiB
TypeScript
/*
|
|
Copyright 2025 New Vector Ltd.
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
Please see LICENSE in the repository root for full details.
|
|
*/
|
|
|
|
import { test } from "vitest";
|
|
import { render, screen } from "@testing-library/react";
|
|
import { type FC, useEffect, useState } from "react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import { TypedEventEmitter } from "matrix-js-sdk";
|
|
|
|
import { useTypedEventEmitterState } from "./useEvents";
|
|
|
|
class TestEmitter extends TypedEventEmitter<"change", { change: () => void }> {
|
|
private state = 1;
|
|
public readonly getState = (): number => this.state;
|
|
public readonly getNegativeState = (): number => -this.state;
|
|
public readonly setState = (value: number): void => {
|
|
this.state = value;
|
|
this.emit("change");
|
|
};
|
|
}
|
|
|
|
test("useTypedEventEmitterState reacts to events", async () => {
|
|
const user = userEvent.setup();
|
|
const emitter = new TestEmitter();
|
|
|
|
const Test: FC = () => {
|
|
const value = useTypedEventEmitterState(
|
|
emitter,
|
|
"change",
|
|
emitter.getState,
|
|
);
|
|
return (
|
|
<>
|
|
<button onClick={() => emitter.setState(2)}>Change value</button>
|
|
<div>Value is {value}</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
render(<Test />);
|
|
screen.getByText("Value is 1");
|
|
await user.click(screen.getByText("Change value"));
|
|
screen.getByText("Value is 2");
|
|
});
|
|
|
|
test("useTypedEventEmitterState reacts to changes made by an effect mounted on the same render", () => {
|
|
const emitter = new TestEmitter();
|
|
|
|
const Test: FC = () => {
|
|
useEffect(() => emitter.setState(2), []);
|
|
const value = useTypedEventEmitterState(
|
|
emitter,
|
|
"change",
|
|
emitter.getState,
|
|
);
|
|
return `Value is ${value}`;
|
|
};
|
|
|
|
render(<Test />);
|
|
screen.getByText("Value is 2");
|
|
});
|
|
|
|
test("useTypedEventEmitterState reacts to changes in getState", async () => {
|
|
const user = userEvent.setup();
|
|
const emitter = new TestEmitter();
|
|
|
|
const Test: FC = () => {
|
|
const [fn, setFn] = useState(() => emitter.getState);
|
|
const value = useTypedEventEmitterState(emitter, "change", fn);
|
|
return (
|
|
<>
|
|
<button onClick={() => setFn(() => emitter.getNegativeState)}>
|
|
Change getState
|
|
</button>
|
|
<div>Value is {value}</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
render(<Test />);
|
|
screen.getByText("Value is 1");
|
|
await user.click(screen.getByText("Change getState"));
|
|
screen.getByText("Value is -1");
|
|
});
|