【问题标题】:When using a functional component, why do parent state changes cause child to re-render even if context value doesn't change使用功能组件时,为什么即使上下文值没有改变,父状态更改也会导致子重新渲染
【发布时间】:2021-08-05 04:11:18
【问题描述】:

我正在将项目从类组件切换到功能组件,并在使用上下文时遇到问题。

我有一个父布局,其中包含一个打开和关闭的导航菜单(通过父级中的状态更改)。这个布局组件还包含一个我通过上下文传递的用户属性。

在切换到功能组件和钩子之前,一切都很好。

我现在看到的问题是,当从布局组件(父级)打开导航菜单时,会导致子组件重新渲染。如果上下文发生变化,我会期待这种行为,但它没有。这也仅在将 useContext 添加到子项时才会发生。

我正在导出带有备忘录的孩子,还尝试用带有备忘录的孩子包装一个容器,但它没有帮助(实际上它似乎导致更多的渲染)。

下面是代码大纲:

AppContext.tsx

export interface IAppContext {
    user?: IUser;
}

export const AppContext = React.createContext<IAppContext>({});

routes.tsx

...
export const routes = <Layout>
    <Switch>
        <Route exact path='/' component={Home} />
        <Route exact path='/metrics' component={Metrics} />
        <Redirect to="/" />
    </Switch>
</Layout>;

布局.tsx

...

const NavItems: any[] = [
    { route: "/metrics", name: "Metrics" }
];


export function Layout({ children }) {
    const aborter = new AbortController();
    const history = useHistory();
    const [user, setUser] = React.useState<IUser>(null);
    const [navOpen, setNavOpen] = React.useState<boolean>(false);
    const [locationPath, setLocationPath] = React.useState<string>(location.pathname);
    const contextValue = {
        user
    };

    const closeNav = () => {
        if (navOpen)
            setNavOpen(false);
    };

    const cycleNav = () => {
        setNavOpen(prev => !prev);
    };

    React.useEffect(() => {
        Fetch.get("/api/GetUser", "json", aborter.signal)
            .then((user) => !aborter.signal.aborted && !!user && setUser(user))
            .catch(err => err.name !== 'AbortError' && console.error('Error: ', err));

        return () => {
            aborter.abort();
        }
    }, []);

    React.useEffect(() => {
        return history.listen((location) => {
            if (location.pathname != locationPath)
                setLocationPath(location.pathname);
        })
    }, [history]);

    const navLinks = NavItems.map((nav, i) => <li key={i}><Link to={nav.route} onClick={closeNav}>{nav.name}</Link></li>);

    return (
        <div className="main-wrapper layout-grid">
                <header>
                    <div className="header-bar">
                        <div className="header-content">
                            <div className="mobile-links-wrapper">
                                <ul>
                                    <li>
                                        <div className="mobile-nav-bars" onClick={cycleNav}>
                                            <Icon iconName="GlobalNavButton" />
                                        </div>
                                    </li>
                                </ul>
                            </div>
                        </div>
                    </div>
                    <Collapse className="mobile-nav" isOpen={navOpen}>
                        <ul>
                            {navLinks}
                        </ul>
                    </Collapse>
                </header>            
            <AppContext.Provider value={contextValue} >
                <main role="main">
                    {children}
                </main>
            </AppContext.Provider>
            <a target="_blank" id="hidden-download" style={{ display: "none" }}></a>
        </div>
    );
}

Metrics.tsx

...
function Metrics() {
   //Adding this causes re-renders, regardless if I use it
   const { user } = React.useContext(AppContext);
...
}

export default React.memo(Metrics);

我有什么遗漏吗?当布局组件中的navOpen状态发生变化时,如何让metrics组件停止渲染?

我已经在路由器中和街区周围尝试了备忘录。我也尝试过移动 contextprovider,但没有成功。

【问题讨论】:

    标签: reactjs react-hooks


    【解决方案1】:

    每次您的Layout 组件呈现时,它都会为contextValue 创建一个新对象:

    const contextValue = {
        user
    };
    

    由于Layout 组件会在您更改导航状态时重新呈现,这会导致上下文值更改为新创建的对象并触发依赖于该上下文的任何组件重新呈现。

    要解决此问题,您可以根据 user 通过 useMemo 钩子更改来记住 contextValue,这应该会在导航状态更改时消除 Metrics 中的渲染:

    const contextValue = React.useMemo(() => ({
       user
    }), [user]);
    

    或者,如果您真的不需要该对象,您可以直接将 user 作为上下文值传递:

    <AppContext.Provider value={user}>
    

    然后像这样访问它:

    const user = React.useContext(AppContext);
    

    从不必要的重新渲染的角度来看,这应该可以完成同样的事情,而不需要useMemo

    【讨论】:

    • 完美!经过数小时的尝试,这两个建议都很有效。谢谢!
    猜你喜欢
    • 2016-03-26
    • 1970-01-01
    • 2019-11-24
    • 2021-10-21
    • 2022-11-04
    • 1970-01-01
    • 1970-01-01
    • 2021-03-15
    • 2017-04-10
    相关资源
    最近更新 更多