13
Feb

Complex Layouts With React and React-Router

I posted a while ago on reddit’s /r/reactjs subreddit about a problem I had in structuring an application I was working with due to the application having multiple different layouts. I never really got an answer though, most people suggesting I use CSS instead to hide what needed to be hidden. To me, this is not a solution. Let’s explore what the problem is, why I don’t believe CSS is the solution and what I’m currently doing to solve it.

The Problem

To put you in context, it arose while I was working on an application we wanted to have on multiple platforms: web, desktop (with Electron probably) and mobile with React Native. Right there, you should be able to see what the problem is, but in case you don’t there’s a nifty little picture right here on the side (or under if you’re on mobile).

layouts

Click here to see it full sized

Yes, I’m shamelessly using Telegram’s Play Store screenshots. Shamelessly.

You can already see part of the problem here. While the mobile version of the application will only show you one screen, either your list of conversations or the specific conversation you’re viewing, the desktop app will always show the list of conversations plus either a placeholder view (the sexiest ever) if you have not selected anything or the actual conversation you have selected.

“But Guyllaume, those guys on reddit were right, you can just have each view hide a given part of your layout and it works.”

I mean, yeah sure, but think about it for a second. React has the advantage that it can (and should) render only what is required in the DOM. Shouldn’t we make use of that as much as possible? Spoiler alert: The answer is yes. We should.

Yeah, But I’m Lazy. Give Me The Solution

Ok.

Yeah it’s that simple, you just had to ask.

The funny thing is I already had all the tools in my project to do exactly what I wanted. React Router allows you to specify named components you can pass your layout component and we can use this to build any kind of layout we want, as complex as they may be.

But first! We need a little utility library: react-responsive. This will allow you to display a given component, based on media queries. You could also use react-if to be honest and as I’m writing this, I’m thinking I would probably suggest it over react-responsive, but since it’s what I used to do this example we’ll go with react-responsive (plus I have never used react-if in production). The important thing is the tool you use must not render an element if it does not need to be.

Then, once you know how you will handle rendering your components, you need to establish a standard for naming your components in your router configuration. For the above layout, I would have:

  • header: For, well, the header
  • footer: Again, self-explanatory
  • primary: This is the component that should always be displayed. In the Telegram example, it would be your list of conversations
  • secondary: The component that is displayed only when something is selected. The actual conversation in our example
  • placeholder: What should the view display when there’s no secondary

Then everything is pretty simple, just have a layout component that will defer to a specific layout depending on the user’s resolution. Then those layout can decide what they display and how. In code, it looks like this:

// Routing configuration
// All my imports

const defaultComponents = {
    footer: FooterSection,
    header: HeaderSection,
}

export default () => (
    <Route path='/'>
        <IndexRedirect to='conversations' />

        <Route component={CoreLayout}>
            <Route
                path='conversations'
                components={{
                    ...defaultComponents,
                    primary: ConversationsList,
                    placeholder: ConversationPlaceholder,
                }}
            />
            <Route
                path='conversations/:id'
                components={{
                    ...defaultComponents,
                    primary: ConversationsList,
                    secondary: Conversation,
                }}
            />
        </Route>

    <Route component={SingleSectionLayout}>
        <Route
            path='*'
            components={{...defaultComponents, content: NotFound}}
          />
        </Route>
    </Route>
)
// CoreLayout.tsx

export interface ThreeSectionLayoutProps {
    header: React.ReactElement<any>
    footer: React.ReactElement<any>
    placeholder: React.ReactElement<any>
    primary: React.ReactElement<any>
    secondary: React.ReactElement<any>
}

export default class CoreLayout extends React.Component<ThreeSectionLayoutProps, any> {
    public render(): React.ReactElement<any> {
        return (
            <MediaQuery query={Breakpoints.mediumMediaQuery()}>
                {(matches) => matches ? <MobileCoreLayout {...this.props} /> : <DesktopCoreLayout {...this.props} />}
            </MediaQuery>
        )
    }
}
// DesktopCoreLayout.tsx

export default class DesktopCoreLayout extends React.Component<CoreLayoutProps, any> {
    public render(): React.ReactElement<any> {
        return (
            <div>
                {this.props.header}
                {this.props.primary}
                {!this.props.secondary ? this.props.placeholder : this.props.secondary}
                {this.props.footer}
            </div>
        )
    }
}
// MobileCoreLayout.tsx

export default class MobileCoreLayout extends React.Component<CoreLayoutProps, any> {
    public render(): React.ReactElement<any> {
        return (
            <div>
                {this.props.header}
                {!this.props.secondary ? this.props.primary : this.props.secondary}
            </div>
        )
    }
}

Simple isn’t? It really is, but I somehow didn’t realize I could compose my views in such a way.

Cool, Thanks For The Tip!

Sure thing internet stranger! However, I’m sure this solution can be made much better, I am but one man after all! So don’t hesitate to send me any solution you use that you feel tackles this issue better than mine!