Often we create solutions with a fixed visual structure. For example, you have a header with a menu, a page-specific body, and a footer. What do we do if some pages need a different structure?
The problem with fixed page structure
To illustrate the problem, I've created an example of a typical React application. The app has a header with a menu, a body, and a footer. It has three different pages: Home, the cat page, and the dog page.
I've used React Router for routing since most people are probably familiar with it.
const FastApp = () => (
<Router>
<PageStructure>
<Switch>
<Route path="/cat"><CatPage/></Route>
<Route path="/dog"><DogPage/></Route>
<Route path="/"><HomePage/></Route>
</Switch>
</PageStructure>
</Router>
);
The page structure looks like this:
const PageStructure = (props: { children: React.ReactNode }) => (
<div className="app">
<div className="structure">
<Header/>
<div className="content">
{props.children}
</div>
<Footer/>
</div>
</div>
);
Each specific page is passed in as children
, and only needs to think about itself.
This looks fine, so what's the problem?
The challenge arises the day the product owner knocks on your door.
We want to change the color of the footer, but only on the dog page.
The dog page itself has no way to affect the footer since it lives in the PageStructure
component.
What can we do? We can just check the location
in PageStructure
!
const PageStructure = (props: { children: React.ReactNode }) => {
const location = useLocation();
const footerColor = location.pathname === "/dog" ? "pink" : "lightgreen";
return (
<div className="app">
<div className="structure">
<Header/>
<div className="content">
{props.children}
</div>
<Footer background={footerColor}/>
</div>
</div>
)
};
The dog page has now contaminated the PageStructure component. It's a hack, but not something we lose sleep over.
Until you get another message from the product owner.
Cat owners like to view cat pictures in fullscreen
Let's roll up our sleeves.
const PageStructure = (props: { children: React.ReactNode }) => {
const location = useLocation();
const footerColor = location.pathname === "/dog" ? "pink" : "lightgreen";
const [fullscreen, setFullscreen] = useState(false);
const showFullscreenButton = location.pathname === "/cat";
const fullscreenButton = <button onClick={() => setFullscreen(!fullscreen)}>Toggle Fullscreen</button>;
if (fullscreen && showFullscreenButton) {
return (
<div>
{fullscreenButton}
{props.children}
</div>
);
}
return (
<div className="app">
<div className="structure">
<Header/>
<div className="content">
{showFullscreenButton && fullscreenButton}
{props.children}
</div>
<Footer background={footerColor}/>
</div>
</div>
)
};
This isn't fun anymore. Let's hope there are no more change requests for the page structure. Here we're spreading page-specific functionality across multiple places in the code. It's starting to smell.
Clean slate
What if each page was completely blank, without any structure from above?
Here's the code for the FreeApp
component. There's no structure coming from above. Each page is responsible for applying the structure itself.
const FreeApp = () => (
<Router>
<Switch>
<Route path="/cat"><CatPage/></Route>
<Route path="/dog"><DogPage/></Route>
<Route path="/"><HomePage/></Route>
</Switch>
</Router>
);
How can we get the pages that should be similar to have the same structure without copying a lot of code? We can create a component to handle the standard pages that have a header and footer:
const StandardPage = (props: { children: React.ReactNode, background?: string }) => {
const { background = "lightgreen", children} = props;
return (
<div className="app">
<div className="structure">
<Header/>
<div className="content">
{children}
</div>
<Footer background={background}/>
</div>
</div>
)
};
Coloring the footer on the dog page then becomes:
const DogPage = () => (
<StandardPage background={"pink"}>
<h1>DOGS</h1>
</StandardPage>
);
Supporting fullscreen only affects the cat page:
const CatPage = () => {
const [fullscreen, setFullscreen] = useState(false);
const fullscreenButton = <button onClick={() => setFullscreen(!fullscreen)}>Toggle Fullscreen</button>
const body = <div>
{fullscreenButton}
<h1>CAT PAGE</h1>
</div>
if (fullscreen) {
return body
} else {
return <StandardPage>{body}</StandardPage>
}
};
Summary
The downside is that there's a bit more code on each page, but that's a small price to pay. By letting each page decide for itself, you achieve the following:
- More understandable code. You can go to any page and find the code for the entire page
- Easier to customize each page without affecting other pages