diff --git a/README.md b/README.md
index 7068c1d..4a584df 100644
--- a/README.md
+++ b/README.md
@@ -142,6 +142,12 @@ Type: `object`
The page component props, which maps to your App `pageProps` prop.
+#### pageKey?
+
+Type: `string`
+
+The page key used to uniquely identify this page. Useful for dynamic routes, where the `Component` is the same, but you still want the page to be re-mounted. For such cases, you may use `router.asPath.replace(/\?.+/, '')`.
+
#### defaultLayout
Type: `ReactElement`
diff --git a/src/LayoutTree.js b/src/LayoutTree.js
index d446b63..d08a48e 100644
--- a/src/LayoutTree.js
+++ b/src/LayoutTree.js
@@ -11,6 +11,7 @@ export default class LayoutTree extends PureComponent {
static propTypes = {
Component: PropTypes.elementType.isRequired,
pageProps: PropTypes.object,
+ pageKey: PropTypes.string,
defaultLayout: PropTypes.element,
children: PropTypes.func,
};
@@ -20,13 +21,14 @@ export default class LayoutTree extends PureComponent {
};
static getDerivedStateFromProps(props, state) {
- const { Component, pageProps } = props;
+ const { Component, pageProps, pageKey } = props;
- const didPageChange = props.Component !== state.Component;
+ const didPageChange = Component !== state.Component || pageKey !== state.pageKey;
const layoutTree = didPageChange ? getInitialLayoutTree(Component, pageProps) : state.layoutTree;
return {
Component,
+ pageKey,
layoutTree,
};
}
@@ -36,18 +38,19 @@ export default class LayoutTree extends PureComponent {
// eslint-disable-next-line react/sort-comp
updateLayoutTree = (layoutTree) => this.setState({ layoutTree });
- getProviderValue = memoizeOne((Component) => ({
+ getProviderValue = memoizeOne((Component, pageKey) => ({
Component,
+ pageKey,
updateLayoutTree: this.updateLayoutTree,
}));
render() {
- const { defaultLayout, Component, pageProps, children: render } = this.props;
+ const { defaultLayout, Component, pageProps, pageKey, children: render } = this.props;
const { layoutTree } = this.state;
- const page = ;
+ const page = ;
const fullTree = createFullTree(layoutTree ?? defaultLayout, page);
- const providerValue = this.getProviderValue(Component);
+ const providerValue = this.getProviderValue(Component, pageKey);
return (
diff --git a/src/with-layout.js b/src/with-layout.js
index 690b14c..cc747d1 100644
--- a/src/with-layout.js
+++ b/src/with-layout.js
@@ -14,7 +14,13 @@ const withLayout = (mapLayoutStateToLayoutTree, mapPropsToInitialLayoutState) =>
mapPropsToInitialLayoutState = toFunction(mapPropsToInitialLayoutState);
return (Component) => {
- const WithLayout = forwardRef((props, ref) => {
+ const WithLayout = forwardRef((_props, ref) => {
+ const { pageKey, props } = useMemo(() => {
+ const { pageKey, ...props } = _props;
+
+ return { pageKey, props };
+ }, [_props]);
+
const initialLayoutStateRef = useRef();
if (!initialLayoutStateRef.current) {
@@ -28,14 +34,17 @@ const withLayout = (mapLayoutStateToLayoutTree, mapPropsToInitialLayoutState) =>
throw new Error('It seems you forgot to include in your app');
}
- const { updateLayoutTree, Component: ProviderComponent } = layoutProviderValue;
+ const { updateLayoutTree, Component: ProviderComponent, pageKey: providerPageKey } = layoutProviderValue;
const [layoutState, setLayoutState] = useObjectState(initialLayoutStateRef.current);
useEffect(() => {
- if (layoutState !== initialLayoutStateRef.current && ProviderComponent === WithLayout) {
+ if (layoutState !== initialLayoutStateRef.current &&
+ ProviderComponent === WithLayout &&
+ providerPageKey === pageKey
+ ) {
updateLayoutTree(mapLayoutStateToLayoutTree(layoutState));
}
- }, [layoutState, updateLayoutTree, ProviderComponent]);
+ }, [layoutState, updateLayoutTree, ProviderComponent, providerPageKey, pageKey]);
return useMemo(() => (
`;
-exports[`should not update layout if page is not the active Component of LayoutTree 1`] = `
+exports[`should ignore setLayoutState calls if Component's pageKey is not the active pageKey of LayoutTree 1`] = `
+
+
+
+
+
+
+ Foo
+
+
+
+
+
+
+
+
+ Foo
+
+
+
+
+`;
+
+exports[`should ignore setLayoutState calls if page is not the active Component of LayoutTree 1`] = `
-
+
-
+
Home
@@ -67,7 +115,7 @@ exports[`should not update layout if page is not the active Component of LayoutT
setLayoutState={[Function]}
>
- Home
+ Foo
@@ -250,3 +298,119 @@ exports[`should render no layout 1`] = `
`;
+
+exports[`should update the layout tree correctly if Component changes 1`] = `
+
+
+
+
+
+
+ Home
+
+
+
+
+
+
+`;
+
+exports[`should update the layout tree correctly if Component changes 2`] = `
+
+
+
+
+
+
+ Foo
+
+
+
+
+
+
+`;
+
+exports[`should update the layout tree correctly if pageKey changes 1`] = `
+
+
+
+
+
+
+ Home
+
+
+
+
+
+
+`;
+
+exports[`should update the layout tree correctly if pageKey changes 2`] = `
+
+
+
+
+
+
+ Home
+
+
+
+
+
+
+`;
diff --git a/test/index.test.js b/test/index.test.js
index da04721..232bb4b 100644
--- a/test/index.test.js
+++ b/test/index.test.js
@@ -3,7 +3,6 @@ import { mount } from 'enzyme';
import { withLayout, LayoutTree } from '../src';
const PrimaryLayout = ({ children }) => { children };
-const SecondaryLayout = ({ children }) => { children };
const Home = () => Home
;
afterEach(() => {
@@ -77,7 +76,6 @@ it('should call layout\'s and page\'s render just once', () => {
it('should render a layout tree correctly based the initial layout state', () => {
const mapLayoutStateToLayoutTree = jest.fn(({ variant }) => );
-
const EnhancedHome = withLayout(mapLayoutStateToLayoutTree, { variant: 'light' })(Home);
const wrapper = mount();
@@ -106,7 +104,7 @@ it('should render a layout tree correctly based the initial layout state (functi
expect(mapPropsToInitialLayoutState).toHaveBeenLastCalledWith({ light: true });
});
-it('should update the layout tree correctly if setLayoutState is called', async () => {
+it('should update the layout tree correctly if setLayoutState is called', () => {
const Home = ({ setLayoutState }) => {
useEffect(() => {
setLayoutState({ variant: 'dark' });
@@ -119,20 +117,57 @@ it('should update the layout tree correctly if setLayoutState is called', async
const HomeMock = jest.fn(Home);
const mapLayoutStateToLayoutTree = jest.fn(({ variant }) => );
-
const EnhancedHomeMock = withLayout(mapLayoutStateToLayoutTree, { variant: 'light' })(HomeMock);
mount();
- await new Promise((resolve) => setTimeout(resolve, 50));
-
expect(PrimaryLayoutMock).toHaveBeenCalledTimes(2);
expect(PrimaryLayoutMock).toHaveBeenNthCalledWith(1, { children: expect.anything(), variant: 'light' }, {});
expect(PrimaryLayoutMock).toHaveBeenNthCalledWith(2, { children: expect.anything(), variant: 'dark' }, {});
expect(HomeMock).toHaveBeenCalledTimes(2);
});
-it('should update the layout tree correctly if setLayoutState is called (function)', async () => {
+it('should update the layout tree correctly if Component changes', () => {
+ const Foo = () => Foo
;
+
+ const EnhancedHome = withLayout()(Home);
+ const EnhancedFoo = withLayout()(Foo);
+
+ const wrapper = mount();
+
+ expect(wrapper).toMatchSnapshot();
+
+ wrapper.setProps({ Component: EnhancedFoo });
+
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should update the layout tree correctly if pageKey changes', () => {
+ let doSetLayoutState = true;
+
+ const Home = ({ setLayoutState }) => {
+ useEffect(() => {
+ doSetLayoutState && setLayoutState({ variant: 'dark' });
+ }, [setLayoutState]);
+
+ return Home
;
+ };
+
+ const mapLayoutStateToLayoutTree = jest.fn(({ variant }) => );
+ const EnhancedHome = withLayout(mapLayoutStateToLayoutTree, { variant: 'light' })(Home);
+
+ const wrapper = mount();
+
+ expect(wrapper).toMatchSnapshot();
+
+ doSetLayoutState = false;
+ wrapper.setProps({ pageKey: 'bar' });
+
+ expect(wrapper).toMatchSnapshot();
+ expect(mapLayoutStateToLayoutTree).toHaveBeenCalledTimes(3);
+});
+
+it('should update the layout tree correctly if setLayoutState is called (function)', () => {
expect.assertions(5);
const Home = ({ setLayoutState }) => {
@@ -151,13 +186,10 @@ it('should update the layout tree correctly if setLayoutState is called (functio
const HomeMock = jest.fn(Home);
const mapLayoutStateToLayoutTree = jest.fn(({ variant }) => );
-
const EnhancedHomeMock = withLayout(mapLayoutStateToLayoutTree, { variant: 'light' })(HomeMock);
mount();
- await new Promise((resolve) => setTimeout(resolve, 50));
-
expect(PrimaryLayoutMock).toHaveBeenCalledTimes(2);
expect(PrimaryLayoutMock).toHaveBeenNthCalledWith(1, { children: expect.anything(), variant: 'light' }, {});
expect(PrimaryLayoutMock).toHaveBeenNthCalledWith(2, { children: expect.anything(), variant: 'dark' }, {});
@@ -222,7 +254,7 @@ it('should fail if LayoutTree was not rendered', () => {
}).toThrow(/it seems you forgot to include/i);
});
-it('should not update layout if page is not the active Component of LayoutTree', () => {
+it('should ignore setLayoutState calls if page is not the active Component of LayoutTree', () => {
jest.spyOn(console, 'error').mockImplementation(() => {});
const Foo = ({ setLayoutState }) => {
@@ -230,11 +262,12 @@ it('should not update layout if page is not the active Component of LayoutTree',
setLayoutState({ variant: 'dark' });
}, [setLayoutState]);
- return Home
;
+ return Foo
;
};
- const EnhancedHome = withLayout()(Home);
- const EnhancedFoo = withLayout(() => )(Foo);
+ const mapLayoutStateToLayoutTree = jest.fn(({ variant }) => );
+ const EnhancedHome = withLayout(mapLayoutStateToLayoutTree, { variant: 'light' })(Home);
+ const EnhancedFoo = withLayout(mapLayoutStateToLayoutTree, { variant: 'light' })(Foo);
const render = jest.fn((tree) => (
<>
@@ -250,4 +283,36 @@ it('should not update layout if page is not the active Component of LayoutTree',
);
expect(wrapper).toMatchSnapshot();
+ expect(mapLayoutStateToLayoutTree).toHaveBeenCalledTimes(1);
+});
+
+it('should ignore setLayoutState calls if Component\'s pageKey is not the active pageKey of LayoutTree', () => {
+ jest.spyOn(console, 'error').mockImplementation(() => {});
+
+ const Foo = ({ foo, setLayoutState }) => {
+ useEffect(() => {
+ foo && setLayoutState({ variant: 'dark' });
+ }, [foo, setLayoutState]);
+
+ return Foo
;
+ };
+
+ const mapLayoutStateToLayoutTree = jest.fn(({ variant }) => );
+ const EnhancedFoo = withLayout(mapLayoutStateToLayoutTree, { variant: 'light' })(Foo);
+
+ const render = jest.fn((tree) => (
+ <>
+ { tree }
+ { }
+ >
+ ));
+
+ const wrapper = mount(
+
+ { render }
+ ,
+ );
+
+ expect(wrapper).toMatchSnapshot();
+ expect(mapLayoutStateToLayoutTree).toHaveBeenCalledTimes(1);
});