I have a simple component with a state variable openInNewWindow
.
As a default value it is set to false:
const [openInNewWindow, setOpenInNewWindow] = useState<boolean>(false);
I have a function where I update it to true
:
const onOpenInNewWindow = () => {
if (openInNewWindow) return;
setOpenInNewWindow(true);
console.log("onOpen - openInNewWindow", openInNewWindow);
window.open(
`${window.location.pathname}editor/${broadcastChannelUUID}?value=${encodeURIComponent(reformatText(value))}&label=${label}${description ? `&description=${description}` : ""}`,
"_blank",
"width=800,height=700,left=200,top=200"
);
};
Here it logs false for the console.log("onOpen - openInNewWindow", openInNewWindow);
, but it logs true
outside of the function after it was called.
The problem I have is that after I update the state variable, I do not get the newest variable state in function onTextChange
:
const onTextChange = (text: string) => {
onChange?.(text);
console.log("onTextChange openInNewWindow", openInNewWindow);
if (openInNewWindow) {
channel.postMessage({ action: "textChange", value: reformatText(text) });
}
};
Even though if I log the value of it in the component it shows true
for it, in the function when it is called it shows false
for it.
Here is the whole component:
export function CustomTextareaEditor({
name,
value,
label,
description,
className,
resize,
readOnly,
onChange,
withOpenInNewWindow,
}: {
name;
value?: string;
label?: string;
description?: string;
className?: string;
resize?: boolean;
readOnly?: boolean;
onChange?: (html: string) => void;
withOpenInNewWindow?: boolean;
}) {
const [openInNewWindow, setOpenInNewWindow] = useState<boolean>(false);
const quillRef = useRef(null);
const broadcastChannelUUID = useMemo(() => crypto.randomUUID(), []);
const channel = useMemo(() => new BroadcastChannel(broadcastChannelUUID), []);
console.log("openInNewWindow in component", openInNewWindow);
useEffect(() => {
const handleMessage = (event) => {
switch (event.data.action) {
case "textChange":
onChange?.(event.data.value);
break;
case "componentUnmounted":
setOpenInNewWindow(false);
break;
}
};
channel.onmessage = handleMessage;
return () => channel.postMessage({ action: "componentUnmounted" });
}, []);
const onTextChange = (text: string) => {
onChange?.(text);
console.log("onTextChange openInNewWindow", openInNewWindow);
if (openInNewWindow) {
channel.postMessage({ action: "textChange", value: reformatText(text) });
}
};
const reformatText = (text?: string) => {
return text?.replace(new RegExp(String.fromCharCode(10), "g"), "<br>");
};
const onOpenInNewWindow = () => {
if (openInNewWindow) return;
setOpenInNewWindow(true);
console.log("onOpen - openInNewWindow", openInNewWindow);
window.open(
`${window.location.pathname}begrunnelse/${broadcastChannelUUID}?value=${encodeURIComponent(reformatText(value))}&label=${label}${description ? `&description=${description}` : ""}`,
"_blank",
"width=800,height=700,left=200,top=200"
);
};
return (
<>
<BodyLong size="small" as="div" className={className}>
{label && (
<Label className="flex items-center gap-2" spacing size="small" htmlFor={name}>
{readOnly && <PadlockLockedFillIcon />} {label}{" "}
{!withOpenInNewWindow && (
<Button
size="xsmall"
variant="tertiary-neutral"
icon={<ExpandIcon title="Ny fane" />}
onClick={() => onOpenInNewWindow()}
type="button"
/>
)}
</Label>
)}
{description && (
<BodyShort spacing textColor="subtle" size="small" className="max-w-[500px] mt-[-0.375rem]">
{description}
</BodyShort>
)}
<CustomQuillEditor
ref={quillRef}
resize={resize}
readOnly={readOnly}
defaultValue={reformatText(value)}
onTextChange={onTextChange}
/>
</BodyLong>
</>
);
}
Not sure why I still get the old value for openInNewWindow
in the function onTextChange
, even after it gets updated?
Even if I wrap both onOpenInNewWindow
and onTextChange
with useCallback
and add a dependency openInNewWindow
, I get old value for openInNewWindow
in onTextChange
when it is called.
const onTextChange = useCallback(
(text: string) => {
onChange?.(text);
console.log("onTextChange openInNewWindow", openInNewWindow);
if (openInNewWindow) {
channel.postMessage({ action: "textChange", value: reformatText(text) });
}
},
[openInNewWindow]
);
const onOpenInNewWindow = useCallback(() => {
if (openInNewWindow) return;
setOpenInNewWindow(true);
console.log("onOpen - openInNewWindow", openInNewWindow);
window.open(
`${window.location.pathname}begrunnelse/${broadcastChannelUUID}?value=${encodeURIComponent(reformatText(value))}&label=${label}${description ? `&description=${description}` : ""}`,
"_blank",
"width=800,height=700,left=200,top=200"
);
}, [openInNewWindow]);
Here is the codesandox with all the code.
You can see that in CustomTextareaEditor
component if you start writing text in the editor, onTextChange
is called, and the variable openInNewWindow
is still false
, even after you open a new window by clicking on button with ExpandIcon
.
Not sure why editor is not rendered in the new window here in codesandbox, but locally it is.
The issue seems to be in the CustomQuillEditor.tsx
, it's how closures work.
If you add the onTextChange
prop as a dependency to the useEffect that uses it it will work as expected, but this is NOT a good solution as it will recreate the Quill editor every time the effect runs.
The way I see it, you can have an useEffect
to initialize Quill and another one to set the on
handler and add the onTextChange
prop as a dependency to it. And don't forget to do quill.off
in the cleanup function;
So, to recap:
CustomQuillEditor.tsx
useEffect
to initialize Quill - you'll probably want to store it in a useState
rather than useRef
, to avoid race conditions in the next effect;useEffect
that will add the on
handler, with the onTextChange
as a dependency - and quill.off
as a cleanup function;onTextChange
in a use callback
;