使用react-router进行React卸载和重新安装布局组件

时间:2020-05-13 15:00:51

标签: reactjs react-router react-router-dom react-router-v4 react-dom

我已经设置了react-router和一个呈现某些Page组件的Switch组件。我所有的组件都呈现相同的布局(此刻,以后将是参数化的)。我的页面组件将内容“注入”到布局中,因此我无法让布局组件包装我的外部开关组件。它必须用作页面组件中的子代。

// Sample code:

// app.js
const App = () => (
    <LanguageProvider defaultLanguage="el" languages={languages}>
        <PathProvider>
            <MenuProvider menu={appMenu}>
                <TemplateProvider injectedComponents={uiComponentOverrides} toolBar={toolBar}>
                    <Switch>
                        <Redirect exact from="/" to="dashboard" />
                        <Route exact path="/dashboard" component={DashboardIndex} />
                        <Route exact path="/overview" component={OverviewIndex} />
                        <Route exact path="/federation" component={FederationIndex} />
                        <Route exact path="/clubs" component={ClubsIndex} />
                        <Route exact path="/athletes" component={AthletesIndex} />
                        <Route exact path="/athletes/:id" component={AthleteDetails} />
                        <Route exact path="/officials" component={OfficialsIndex} />
                        <Route component={EmptyPage} />
                    </Switch>
                </TemplateProvider>
            </MenuProvider>
        </PathProvider>
    </LanguageProvider>
);


// Sample of a page component
// dashboard-page.js

export const DashboardIndex = () => {
    const { t } = useTranslation();
    return (
        <AppFullPage key="page-component" title={t('Home.Index.WelcomeTitle')}>
            <p>Dashboard content</p>
        </AppFullPage>
    );
};

// The AppFullPage component is the layout component. Content is injected to it through the title, subtitle, toolbar, menu and children props.

即使最终的html渲染与布局骨架相同(仅在布局的 slot 部分内部不同),也要进行卸载并重新安装整个布局树。

我可以理解重新渲染整个树的反应。 React看到了一个不同的顶部组件(页面组件在每个位置都不同)。因此react渲染了新组件并创建了一个新的虚拟dom。

我无法理解的是卸载和重新安装顶部html元素,因为它们没有发生变化。最高div不变。我的标题菜单中的语言选择器下拉菜单没有更改。左侧的应用程序菜单未更改。是的,它们是从不同的组件渲染的,但是它们是相同的输出。

如果react比较连续渲染的输出并且仅更改需要的内容,那么为什么要卸载并重新安装组件?

此行为会破坏动画和组件的其他行为(例如,即使页面更改后,我希望公用布局空间中的下拉列表也保持打开状态,但是由于已卸载并重新安装它,因此会丢失状态)。

如何指示我不要重新安装相同的组件?

我如何实现让页面在布局中注入内容的目标?

使用门户网站是解决方案的候选人吗?

3 个答案:

答案 0 :(得分:0)

我们可以看到您的布局组件吗?您确定要卸载/重新安装或重新渲染吗?

如果要卸载组件-应该有某种条件的渲染,请检查该位置。如果组件重新渲染,请检查道具,以确保它们在必要时不会改变。

答案 1 :(得分:0)

将常用的布局组件移到开关之外。然后使用该位置确定您的动态道具。

<AppFullPage>
  <Switch>
    {all routes}
  </Switch>
</AppFullPage>
const AppFullPage = () => {
  const location = useLocation();
  const [title, setTitle] = useState('');
  const { t } = useTranslation();

  useEffect(() => {
    switch(location.pathname) {
      case 'dashboard': {
        setTitle(t('Home.Index.WelcomeTitle'));
      }
      ... the rest of your cases
    }
  }, [location.pathname]);

}

此代码只是一个示例,如果它可以完全按原样运行但应该给您一个想法

,则为idk

答案 2 :(得分:0)

我要使用的解决方案如下。 这是一个黑客解决方法,可让嵌套页面在上述级别为布局提供内容。我会尝试改善它。

我正在做的是创建一个上下文(LayoutContext),该上下文将按插槽名称保存每个插槽的渲染元素。 Layout组件保留上下文的状态,并将节设置器传递到上下文的后代。

我创建了助手组件(LayoutSectionPlaceHolderLayoutSection),以在需要的地方封装钩子和备注的使用。

Layout组件中,我们在每个我们希望页面注入内容的地方渲染一个LayoutSectionPlaceHolder元素。

每个页面组件应返回一个或多个LayoutSection元素,并且仅返回一个。我们不希望页面本身呈现任何内容。我们只希望它在内容更改时将内容推送(注入)到布局。 LayoutSection组件负责将作为子项道具提供的内容注入Layout

此解决方案是一种破解方法,因为在页面的第一个呈现中,所有插槽都是空的(尚未设置任何内容)。这是因为第一次安装内容是在安装页面元素时使用useLayoutEffect钩子,然后在每次内容更改时提供。

话虽这么说,如果React提供一种反向上下文(从子级到父级)的内置机制作为控制模式的反转,那就太好了

import React, { createContext, useCallback, useContext, useLayoutEffect, useReducer, useState } from 'react';
import { Route, Switch } from 'react-router';
import { BrowserRouter, Link } from 'react-router-dom';
function App() {
    return (
        <BrowserRouter>
            <Switch>
                <Route exact path='/page1' render={() => <Layout><Page1 /></Layout>} />
                <Route exact path='/page2' render={() => <Layout><Page2 /></Layout>} />
            </Switch>
        </BrowserRouter>
    );
}

const LayoutSlotSetterContext = createContext(() => {});

const useLayoutSlotSetter = () => {
    const { setSection } = useContext(LayoutSlotSetterContext);
    return setSection;
};

const useLayoutSectionUpdater = (slotName, content, slotSetter) => useLayoutEffect(() => slotSetter(slotName, content), [content, slotName, slotSetter]);
const useLayoutSectionContent = (sectionName) => {
    const { sections: { [sectionName]: section } = {} } = useContext(LayoutSlotSetterContext);
    return section;
};

const LayoutSectionPlaceHolder = ({ section }) => {
    const content = useLayoutSectionContent(section);
    return content === undefined ? null : content;
};

const LayoutSection = ({ section, children }) => {
    const slotSetter = useLayoutSlotSetter();
    useLayoutSectionUpdater(section, children, slotSetter);
    return null;
};

const Layout = ({ children }) => {
    useLayoutEffect(() => () => console.log('unmounted: [Layout]'), []);
    const reducer = useCallback((sections, { section, content }) => {
        if (!section) return sections;
        return {
            ...sections,
            [section]: content,
        };
    }, []);
    const [sections, dispatch] = useReducer(reducer, {});
    const setSection = useCallback((section, content) => dispatch({ section, content }), [dispatch]);

    return (
        <LayoutSlotSetterContext.Provider value={{ sections, setSection }}>
            {children}
            <Menu /> {/* <-- this element is the same for every page. It should never unmounted */}
            <Header /> {/* <-- this element is the same for every page. It should never be unmounted */}
            <div>
                <LayoutSectionPlaceHolder section='content' />
            </div>
            <div>
                <LayoutSectionPlaceHolder section='secondsMounted' /> seconds mounted [<LayoutSectionPlaceHolder section='page' />]
            </div>
        </LayoutSlotSetterContext.Provider>
    );
};

const Menu = () => {
    useLayoutEffect(() => () => console.log('unmounted: [Menu]'), []);
    return (
        <ul>
            <li>
                <Link to='/page1'>Page 1</Link>
            </li>
            <li>
                <Link to='/page2'>Page 2</Link>
            </li>
        </ul>
    );
};

const Header = () => {
    useLayoutEffect(() => () => console.log('unmounted: [Header]'), []);
    const counter = useSecondsMounted();
    return <h2>{counter} seconds mounted [header]</h2>;
};

const Page1 = () => {
    const counter = useSecondsMounted();
    return (
        <>
            <LayoutSection section='content'>
                <p>Page 1 content</p>
            </LayoutSection>
            <LayoutSection section='page'>Page 1</LayoutSection>
            <LayoutSection section='secondsMounted'>{counter}</LayoutSection>
        </>
    );
};

const Page2 = () => {
    const counter = useSecondsMounted();
    return (
        <>
            <LayoutSection section='content'>
                <p>Page 2 content</p>
            </LayoutSection>
            <LayoutSection section='page'>Page 2</LayoutSection>
            <LayoutSection section='secondsMounted'>{counter}</LayoutSection>
        </>
    );
};

/** Counts the seconds a component stays mounted */
const useSecondsMounted = () => {
    const [counter, setCounter] = useState(0); // used for illustration purposes. The counter increases +1/sec. It should do that as long as it is visible in the page
    useLayoutEffect(() => {
        const interval = setInterval(() => setCounter((c) => c + 1), [1000]);
        return () => clearInterval(interval);
    }, []);
    return counter;
};

export default App;