为什么从本地存储获取新值时 className(带条件运算符)不会更改?

Why className (with conditional operator) doesn't change when getting new value from Local Storage?

提问人:iwishyoujoy 提问时间:11/14/2023 最后编辑:T.J. Crowderiwishyoujoy 更新时间:11/15/2023 访问量:41

问:

主要思想:我有一个菜单,我想存储指示在本地存储中打开菜单的变量。

起初,我编写了用于获取和设置 LocalStorage 变量的函数:

const getItem = (key, defaultValue) => {
    // getting item, if undefined/null return defaultValue
};

const setItem = (key, value) => {
    // just setting item, works perfect
};

然后我实现了自定义钩子,我在其中使用了这些函数。代码如下所示:

export const useMenu = () => {
    const isOpened = Boolean(getItem("isOpened", true));
    const toggleMenu = useCallback(() => {
        setItem("isOpened", !isOpened);
    }, [isOpened]);
    
    // ...

    return {
        isOpened,
        toggleMenu,
    };
};

移动到带有组件的主文件。我用钩子获取变量:

const { isOpened, toggleMenu } = useMenu();

然后在主容器中,我有 className 的条件运算符:

<div className={isOpened ? styles.openedContainer : styles.closedContainer}>

在容器内,我通过单击切换变量:

<div onClick={toggleMenu}>

主要问题是我的菜单没有关闭!这意味着 className 不会更改。但与此同时,onClick 事件运行良好,正如我在浏览器控制台中看到的那样,isOpened 变量会按时更改。有什么原因会发生吗?

关于localStorage类型:是的,我知道在localStorage中您只能存储字符串。当我从 localStorage 获取一个项目时,我将其转换为布尔类型。我在控制台中检查了typeof isOpened,它似乎是一个布尔值。

const { useCallback } = React;

const fakeStorage = new Map();

const styles = {
    openedContainer: "opened",
    closedContainer: "closed",
};

const getItem = (key, defaultValue) => {
    const value = fakeStorage.get(key);
    // Sadly, Stack Snippets with transpiling enabled don't understand
    // nullish coalescing, have to go old-school
    if (typeof value === "undefined") {
        return defaultValue;
    }
    return value;
};

const setItem = (key, value) => {
    fakeStorage.set(key, value);
};

/*export*/ const useMenu = () => {
    const isOpened = Boolean(getItem("isOpened", true));
    const toggleMenu = useCallback(() => {
        setItem("isOpened", !isOpened);
    }, [isOpened]);
    
    // ...

    return {
        isOpened,
        toggleMenu,
    };
};

const Example = () => {
    const { isOpened, toggleMenu } = useMenu();
    return (
        <div className={isOpened ? styles.openedContainer : styles.closedContainer}>
            <div onClick={toggleMenu}>toggle</div>
        </div>
    );
};

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Example />);
.opened {
    color: green;
}
.closed {
    color: red;
}
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>

javascript reactjs react-hooks 本地存储

评论


答:

2赞 T.J. Crowder 11/14/2023 #1

问题在于,更改不会改变 React 用来知道何时重新渲染组件的任何内容。它超出了 React 所知道的范围。

你可以把它带入 React 所知道的事物中,同时将它保留在状态成员中;下面是一个内置的示例:useState

/*export*/ const useMenu = () => {
    const [isOpened, setIsOpened] = useState(Boolean(getItem("isOpened", true)));

    const toggleMenu = useCallback(() => {
        const updated = !isOpened;
        setItem("isOpened", updated);
        setIsOpened(updated);
    }, [isOpened]);
    
    // ...

    return {
        isOpened,
        toggleMenu,
    };
};

const { useCallback, useState } = React;

const fakeStorage = new Map();

const styles = {
    openedContainer: "opened",
    closedContainer: "closed",
};

const getItem = (key, defaultValue) => {
    const value = fakeStorage.get(key);
    // Sadly, Stack Snippets with transpiling enabled don't understand
    // nullish coalescing, have to go old-school
    if (typeof value === "undefined") {
        return defaultValue;
    }
    return value;
};

const setItem = (key, value) => {
    fakeStorage.set(key, value);
};

/*export*/ const useMenu = () => {
    const [isOpened, setIsOpened] = useState(Boolean(getItem("isOpened", true)));

    const toggleMenu = useCallback(() => {
        const updated = !isOpened;
        setItem("isOpened", updated);
        setIsOpened(updated);
    }, [isOpened]);
    
    // ...

    return {
        isOpened,
        toggleMenu,
    };
};

const Example = () => {
    const { isOpened, toggleMenu } = useMenu();
    return (
        <div className={isOpened ? styles.openedContainer : styles.closedContainer}>
            <div onClick={toggleMenu}>toggle</div>
        </div>
    );
};

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Example />);
.opened {
    color: green;
}
.closed {
    color: red;
}
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>

有很多方法可以旋转它。在那里,我将本地存储与状态变量并行。您还可以通过以下方式驱动状态变量(并使用回调来更新状态,以避免不得不用作依赖项useEffectisOpeneduseCallback);

/*export*/ const useMenu = () => {
    const [isOpened, setIsOpened] = useState(Boolean(getItem("isOpened", true)));

    useEffect(() => {
        // Update local storage
        setItem("isOpened", isOpened);
    }, [isOpened]);

    const toggleMenu = useCallback(() => {
        setIsOpened((flag) => !flag);
    }, []);
    
    // ...

    return {
        isOpened,
        toggleMenu,
    };
};

const { useCallback, useState, useEffect } = React;

const fakeStorage = new Map();

const styles = {
    openedContainer: "opened",
    closedContainer: "closed",
};

const getItem = (key, defaultValue) => {
    const value = fakeStorage.get(key);
    // Sadly, Stack Snippets with transpiling enabled don't understand
    // nullish coalescing, have to go old-school
    if (typeof value === "undefined") {
        return defaultValue;
    }
    return value;
};

const setItem = (key, value) => {
    fakeStorage.set(key, value);
};

/*export*/ const useMenu = () => {
    const [isOpened, setIsOpened] = useState(Boolean(getItem("isOpened", true)));

    useEffect(() => {
        // Update local storage
        setItem("isOpened", isOpened);
    }, [isOpened]);

    const toggleMenu = useCallback(() => {
        setIsOpened((flag) => !flag);
    }, []);
    
    // ...

    return {
        isOpened,
        toggleMenu,
    };
};

const Example = () => {
    const { isOpened, toggleMenu } = useMenu();
    return (
        <div className={isOpened ? styles.openedContainer : styles.closedContainer}>
            <div onClick={toggleMenu}>toggle</div>
        </div>
    );
};

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Example />);
.opened {
    color: green;
}
.closed {
    color: red;
}
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>

评论

0赞 iwishyoujoy 11/15/2023
问题是我使用此菜单来浏览不同的页面。将 isOpened 切换为 false 并单击另一个页面后,className 刷新并使用 isOpened=true(初始状态),但在控制台中我可以看到 isOpened=false。有什么方法可以让它工作吗?
0赞 T.J. Crowder 11/15/2023
@iwishyoujoy - 初始状态不应是当您使用提供的代码转到新页面时。初始状态不是 ,而是 。 如果已设置为 ,则应返回。truetrueBoolean(getItem("isOpened", true))getItemfalseisOpenedfalse
0赞 iwishyoujoy 11/15/2023
好。但我不知道为什么当我浏览菜单(单击到其他页面)时,isOpened 的值会保存,但 main div 中的 className 使用相反的值,所以我仍然可以看到页面的标题,在控制台中我可以看到 isOpened 设置得像它应该的那样,但菜单的行为不尽如人意(由于错误的 className)。是重新渲染的东西吗?
0赞 T.J. Crowder 11/15/2023
@iwishyoujoy - 我无法帮你调试我看不到的东西。:-)提出的问题是关于在更改组件时使组件重新渲染,本答案涵盖了这一点。如果您对其他组件似乎从本地存储中获得错误的初始条件有另一个问题,那就是另一个问题,并且需要有一个最小的可重现示例来显示它发生,以便人们可以帮助您弄清楚。