最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

reactjs - State variable not getting newest state in a component function - Stack Overflow

programmeradmin1浏览0评论

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.

Share Improve this question edited Feb 4 at 19:05 Leff asked Feb 4 at 13:04 LeffLeff 1,37031 gold badges111 silver badges226 bronze badges 15
  • 1 This question is similar to: The useState set method is not reflecting a change immediately. If you believe it’s different, please edit the question, make it clear how it’s different and/or how the answers on that question are not helpful for your problem. – David Commented Feb 4 at 13:10
  • Though there's probably more issues here beyond the proposed duplicate. For example, that onTextChange may need to be wrapped in a useCallback, and that useEffect 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:15
  • Even if I wrap both onOpenInNewWindow and onTextChange with useCallback and add a openInNewWindow as a dependency, I get old state values. Will update question with the newest code. – Leff Commented Feb 4 at 13:21
  • You fot to also include dependencies in the useEffect. 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 a useEffect or useCallback depends on that may change from one render to the next should be in its dependency array. – David Commented Feb 4 at 13:31
  • I am not sure why do I need to have a dependecy in useEffect, since I am only setting listeners there, if I am calling onTextChange directly, and I already have a dependency set there in useCallback? This onTextChange is not called from useEffect in any case, that was my point. – Leff Commented Feb 4 at 13:51
 |  Show 10 more comments

1 Answer 1

Reset to default 1

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 fet to do quill.off in the cleanup function;

So, to recap:

  1. Go to CustomQuillEditor.tsx
  2. Add a 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;
  3. Add another useEffect that will add the on handler, with the onTextChange as a dependency - and quill.off as a cleanup function;
  4. Make sure to wrap onTextChange in a use callback;
发布评论

评论列表(0)

  1. 暂无评论