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.
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.
1 Answer
Reset to default 1The 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 fet to do quill.off
in the cleanup function;
So, to recap:
- Go to
CustomQuillEditor.tsx
- Add a
useEffect
to initialize Quill - you'll probably want to store it in auseState
rather thanuseRef
, to avoid race conditions in the next effect; - Add another
useEffect
that will add theon
handler, with theonTextChange
as a dependency - andquill.off
as a cleanup function; - Make sure to wrap
onTextChange
in ause callback
;
onTextChange
may need to be wrapped in auseCallback
, and thatuseEffect
operation should probably have some values/functions in its dependency array. (I'm surprised you're not getting warnings about that.) – David Commented Feb 4 at 13:15useEffect
. The effect only runs when those dependencies change. So if the value of a dependency (e.g.openInNewWindow
) is changing then the effect would need to re-run with the new value. If you're not getting warnings about this from a linter/transpiler/whatever in the build process then you'll probably want to look into that. Anything auseEffect
oruseCallback
depends on that may change from one render to the next should be in its dependency array. – David Commented Feb 4 at 13:31