Introduction

To be able to fully benefit from this article you've got to have some experience with both CSS Grid and React framework.

I prepared a small demo of what can be achieved with this technique in my sandbox. I'll try to walk you through it with a high level overview.

The app in sandbox has a few different layouts based on a page that is currently visited and the size of the viewport. Layouts contain a set of grid areas that are named the same across all of them. Try to play around by clicking the links in the app or changing the viewport from mobile to desktop to spot differences.

In essence I tried to find a way to link a React component with a CSS grid area but to not share layout details with each and every component. They must be dumb about where they are put.

Shell

The most important piece of functionality here is contained under Shell folder. Shell is responsible for determining what layout a currently rendered screen should have. Shell is aware of the location and viewport size:

const location = useLocation();
const isMd = useMedia("(min-width: 768px)");

It keeps locally what layout is currently aplied to a page:

const [preparedLayout, setLayout] = React.useState(null);

A side effect is fired whenever the viewport or location changes and it dynamically fetches the layout config for the page or it falls back to the basic one:

React.useEffect(() => {
  import(`./layoutConfig/layout${location.pathname.replace(/\//g, ".")}`)
    .catch(() => import("./layoutConfig/layout.basic"))
    .then((layout) => setLayout(isMd ? layout.md : layout.sm));
}, [location, isMd]);

Then Shell applies the acquired config to the markup by setting CSS Grid properties on the root element:

style={{
  gridTemplateAreas: preparedLayout?.gridAreas,
  gridTemplateColumns: preparedLayout?.columns,
  gridTemplateRows: preparedLayout?.rows
}}

Inside the root element, the condition is placed to check if the fetched layout config contains areasToComponentsMap and if so, ConfigBasedComponents component is rendered. Otherwise InlinedComponents.

InlinedComponents

Let's move into InlinedComponents first. It's a simple React.Children.map over the passed children. Every child needs to have gridArea prop defined so that it's finally wrapped with an element that has gridArea CSS property actually defined. This is to have separation between a component that doesn't need to know anything about grid and the calling side where it's declared.

InlinedComponents uses children passed directly to it and this is the case in main index file:

<Shell path="/*">
  <Logo1 gridArea="logo" />
  <TopNav gridArea="top-nav" />
  <SecondaryNav gridArea="secondary-nav" />
  <DropdownMenuButton gridArea="dropdown-menu-button" />
  <div gridArea="search-field"></div>
  <div gridArea="top-area"></div>
  <App gridArea="main" />
</Shell>

This is one way of rendering components in a grid. It's easier and more static because components are linked to grid areas in JSX.

layout.basic.json

Now let's take a look at layout.basic.json to see how does Shell know where to put each component into the grid:

{
"sm": {
  "columns": "1fr 1fr 1fr 1fr 1fr",
  "rows": "auto auto 1fr auto",
  "gridAreas": "'logo logo secondary-nav secondary-nav search-field' 'top-area top-area top-area top-area top-area' 'main main main main main' 'top-nav top-nav top-nav . dropdown-menu-button'"
},
"md": {
  "columns": "1fr 1fr 1fr 1fr 1fr 1fr 1fr",
  "rows": "auto auto 1fr",
  "gridAreas": "'logo logo logo . top-nav top-nav dropdown-menu-button' 'secondary-nav secondary-nav search-field search-field search-field top-area top-area' 'main main main main main main main'"
}
}

Here we can see stadard syntax for CSS Grid columns, rows and areas but additionally divided per viewport size. I've done this to show the possibilities with the approach how to tackle grid in a React app. Grid area names are arbitrary but they match props defined on Shell children.

ConfigBasedComponents

Now let's move to the other, more dynamic way of configuring grid in a React app thanks to areasToComponentsMap in layout.news.business.json:

"areasToComponentsMap": {
"logo": "../Logo2",
"top-nav": "../TopNav",
"secondary-nav": "../SecondaryNav",
"dropdown-menu-button": "../DropdownMenuButton",
"search-field": "../SearchField",
"top-area": "../HotNewsBusiness",
"main": "../App"
}

This layout is loaded when you head over to news/business. The definition of this layout contains a map of grid areas to relative component paths. This is a highly dynamic way of completely restructuring an app screen by lazily loading React components and laying them out in a new way.

Let's take a look at ConfigBasedComponents. This shell feature walks through every pair of areasToComponentsMap, lazily loads defined component with React.lazy and wraps the final component with an element that has CSS grid-area property set:

const LazyComponent = React.lazy(() =>
  import(component).then((module) => ({
    default: module[component.substring(3)]
  }))
);

return (
  <div
    key={area}
    style={{
      gridArea: area,
      textAlign: "center",
      backgroundColor: `#${palette[k]}`
    }}
  >
    {area} area

    <React.Suspense fallback={<div />}>
      <LazyComponent />
    </React.Suspense>
  </div>
);
}

So the final outcome is similar to what can be achieved with InlinedComponents but it is more dynamic and highly configurable.

Final Thoughts

I hope you liked this article and the linked example. It's very barebone but it definitely shows how a React app that uses CSS grid could look like. Cheers!