ODINODIN

Page structure

2021-11-01

frontend

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.

Page structure

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.

Page structure

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?

Page structure

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