DEV Community

loading...
Cover image for React Render Props with Ts & styled-components | Part 2

React Render Props with Ts & styled-components | Part 2

didof profile image Francesco Di Donato ・7 min read

In the previous post we built a React component capable of building a grid. Thanks to the render props method, it exposes the width available for each child.

In this post we will improve it, first of all by creating a component to use inside it. Then, we will automate the grid creation process thanks to a builder method.

you can find the code in repo


Create in src/components/GridElement

  • GridElement.types.ts
  • GridElement.styles.ts
  • GridElement.tsx

What we want

We want a component that acts as an adapter between the GridLayout and what we actually want to put into it. Therefore it will accept a content. A second prop, style, will categorically contain the width, leaving optional the addition of height and backgroundColor (the latter could have a fallback on the backgroundColor of the theme if you were using ThemeProvider upstream.

Per semplicità, non applichiamo il ThemeProvider.

GridElement.types.ts
export interface GridElementProps {
    style: {
        width: number;
        height?: number;
        backgroundColor?: string;
    }
    content: any;
}
Enter fullscreen mode Exit fullscreen mode

Let's move on to the styled-component. The Prop it receives (optionally or not) mimics those previously decided. It will center the content. The width is decided from the outside (it will be the one provided by the GridLayout). The height and backgroundColor are applied only if they were provided thanks to the &&.

GridElement.styles.ts
import styled from 'styled-components'

type WrapperProps = {
    width: number;
    height?: number;
    backgroundColor?: string;
}

export const GridElementWrapper = styled.div<WrapperProps>`
  display: flex;
  justify-content: center;
  align-items: center;

  width: ${({ width }) => `${width}px`};
  ${({ height }) => height && `height: ${height}px`};
  ${({ backgroundColor }) => backgroundColor && `background-color: ${backgroundColor}`};
Enter fullscreen mode Exit fullscreen mode

Finally, here we are at the GridElement.tsx. It's very simple: since it receives the style props in a single style package we can simply spread it thanks to the spread operator. The content is inserted inside.

GridElement.tsx
import { GridElementProps } from './GridElement.types'
import { GridElementWrapper } from './GridElement.styles'

const GridElement = ({ style, content }: GridElementProps): JSX.Element => {
    return (
        <GridElementWrapper
            {...style}
        >
            {content}
        </GridElementWrapper>
    )
}

export default GridElement
Enter fullscreen mode Exit fullscreen mode

So let's use it in conjunction with the GridLayout component.

App.tsx
import React from 'react'
import GridElement from './components/GridElement/GridElement';
import GridLayout from './components/GridLayout/GridLayout'

const colors = ['green', 'white', 'red']

const App = () => (
  <div>
    <h1>React Render Props Grid Layout</h1>
    <GridLayout
      columnsAmount={3}
    >
      {(itemWidth) => React.Children.map(colors, (color, index) => (
        <GridElement
          style={{
            width: itemWidth,
            height: 100,
            backgroundColor: color
          }}
          content={index}
        />
      ))}
    </GridLayout>
  </div>
)

export default App;
Enter fullscreen mode Exit fullscreen mode

And the result is:
Output

Yeah, ok, cool. But looking at that heap of code it immediately comes to mind that we can do better.
Suppose we want all elements to have the same measurements. We could delegate this information to the GridLayout itself. We could also use GridElement directly inside it, thus avoiding having to mess with the App.tsx code (or wherever GridLayout is used).


The Plan

GridLayout will be able to accept two new props:

  • items: a list of configuration objects, each representing the element to put in the grid and its relative styling.
  • builder: a method that describes the rendering of each element passed into items; the strength lies in the fact that (covertly) wraps each element with GridElement, thus automatically managing its style.

The smartest kids will have guessed that many things can be managed in this way, not just style.

GridLayout.types.ts
export interface GridLayoutProps {
    columnsAmount: number;
    children?: (width: number) => JSX.Element | JSX.Element[]
    rowHeight?: number;
    items?: configItems[];
    builder?: (item: JSX.Element) => JSX.Element;
}

export interface configItems {
    style: {
        width?: number,
        height?: number,
        backgroundColor?: string
    };
    component: JSX.Element
}
Enter fullscreen mode Exit fullscreen mode

Using the builder method implies that the render props of children will not be used - not surprisingly both are optional. The management of one or the other will be managed in the component itself.
Regarding the list of configuration objects, the component property is the component itself. style, on the other hand, allows for a specific styling to be applied to a specific element. However, it is completely optional - in case of omission, the styling of the element will be the default one (width = elementWidth, backgroundColor possibly provided by the Theme, after adapting GridElementWrapper. But let's not waste attention).


As in the previous post, the sheer size of the next component could make you want to read off. So let's break down the concepts:

  1. guard against the simultaneous use of children and builders.
  2. forced to supply items when the builder was used.
  3. Conditional use of children or use of the builder method within the content of the GridElement.

The rest remains unchanged from the previous post. See you later the block of code.

GridLayout.tsx
import React, { useState, useEffect, createRef, ReactElement } from 'react'
import { GridLayoutProps } from './GridLayout.types'
import { Grid } from './GridLayout.styles'
import GridElement from '../GridElement/GridElement'

const GridLayout = ({ children, columnsAmount, rowHeight, builder, items }: GridLayoutProps): ReactElement => {
    if (typeof children === 'undefined' && typeof builder === 'undefined') {
        throw new Error('Either children or builder is required')
    }
    if (typeof builder !== 'undefined' && (typeof items === 'undefined' || items.length < 0)) {
        throw new Error('When using builder your should provide also items')
    }

    const gridRef = createRef<HTMLDivElement>()
    const [elementWidth, setElementWidth] = useState<number>(0);

    useEffect(() => {
        const { current } = gridRef
        let gridWidth = current!.getBoundingClientRect().width
        setElementWidth(Math.round(gridWidth / columnsAmount));
    }, [columnsAmount, rowHeight, gridRef])

    return (
        <Grid
            columnsAmount={columnsAmount}
            rowHeight={rowHeight}
            ref={gridRef}
        >
            {Boolean(children) && children!(elementWidth)}
            {Boolean(builder) && items!.map(({ component, style }, index) => {
                const { width, height, backgroundColor } = style
                console.log(height)
                return (
                    <GridElement
                        key={index}
                        style={{
                            width: width || elementWidth,
                            height,
                            backgroundColor,
                        }}
                        content={builder!(component)}
                    />
                )
            })}
        </Grid>
    )
}

export default GridLayout
Enter fullscreen mode Exit fullscreen mode

1. guard against the simultaneous use of children and builders

if (typeof children === 'undefined' && typeof builder === 'undefined') {
        throw new Error('Either children or builder is required')
    }
Enter fullscreen mode Exit fullscreen mode

Obviously, if the two methods coexisted, we could have given priority to one or the other. But no.

2. forced to supply items when the builder was used

if (typeof builder !== 'undefined' && (typeof items === 'undefined' || items.length < 0)) {
        throw new Error('When using builder your should provide also items')
    }
Enter fullscreen mode Exit fullscreen mode

Because one without the other makes no sense to exist. Where I come from it is said that they are culo & camicia.

3. Conditional **use of children* or use of the builder method within the content of the GridElement*

{Boolean(children) && children!(elementWidth)}
Enter fullscreen mode Exit fullscreen mode

Apart from a check on the existence of the method, it does nothing new. Rather, the other:

{Boolean(builder) && items!.map(({ component, style }, index) => {
                const { width, height, backgroundColor } = style
                console.log(height)
                return (
                    <GridElement
                        key={index}
                        style={{
                            width: width || elementWidth,
                            height,
                            backgroundColor,
                        }}
                        content={builder!(component)}
                    />
                )
            })}
Enter fullscreen mode Exit fullscreen mode

This is the important point. Keeping in mind that either children exist, or builder exists, only one of the two cases will be executed.
In this case the configuration objects are mapped to a set of GridElements. The eventual style of each object modifies the style of the GridElement which in any case knows what to do in the absence of the first.
The builder method is placed in the content of the GridElement and is passed the component itself.

If you are wondering why we are manually mapping items then explicitly passing the key rather than using React.Children.map(), the reason is that the latter must act on a component list. But here we have a list of items.


Now let's stress our GridLayout with a series of ugly squares to look at. Let's see if it breaks!

We have a list of colors. Let's map them to a list of configuration objects where the component will be a simple

with the index inside. Could be any custom component. The style is here a pile of junk, just to see how it behaves:
  • width: 100 multiplied by the index, but only for even numbers; otherwise fallback to default;
  • height: base 30 plus the index squared multiplied by 20.
  • backgroundColor: the element being mapped.
App.tsx
import React from 'react'
import GridLayout from './components/GridLayout/GridLayout'
import { configItems } from './components/GridLayout/GridLayout.types'

const colors = ['green', 'yellow', 'red', 'blue', 'orange']

const elements: configItems[] = colors.map((color, index) => ({
  component: <div>{index}</div>,
  style: {
    width: index % 2 == 0 ? 100 * index : undefined,
    height: 20 * Math.pow(index, 2) + 30,
    backgroundColor: color
  }
}))

const App = () => (
  <div>
    <h1>React Render Props Grid Layout - The Return</h1>
    <GridLayout
      columnsAmount={3}
      items={elements}
      builder={(item) => (
        <div>{item}</div>
      )}
    />
  </div>
)

export default App;

Let's pass this list of configuration objects to GridLayout. So in the builder we only get the component of each one back - the style has been handled internally.

Alt Text

Ugly, innit? But only because I wanted to make explicit the versatility of what we have built. I'm sure you understand the possibilities.


Post Credit

I will not dwell further but I leave a beginning: what if we passed another prop (not mandatory) to GridLayout? What if this defines the type of animation to be applied to the mounting of each element? I believe that framer-motion goes perfectly with styled-components.
What if we managed these props from a centralized store so that we could adjust them as needed?

Now we are asking the right questions ...

Useful links

Connect with Me

GitHub - didof
LinkedIn - Francesco Di Donato
Twitter - @did0f

Discussion (0)

pic
Editor guide