React Render Props with Ts & styled-components | Part 2
Tags: react, typescript, layout, styledcomponents
Published: 2021-01-16 13:18:12

In the [previous post](https://dev.to/didof/render-props-1-grid-layout-4mo6) 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](https://github.com/didof/react-render-props-grid-layout/tree/render-props-2)

***

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](https://styled-components.com/docs/advanced) upstream.

> Per semplicità, non applichiamo il ThemeProvider.

#####`GridElement.types.ts`
```ts
export interface GridElementProps {
    style: {
        width: number;
        height?: number;
        backgroundColor?: string;
    }
    content: any;
}
```

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`
```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}`};
```

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`
```ts
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
```

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

#####`App.tsx`
```ts
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;
```

And the result is:
![Output](https://dev-to-uploads.s3.amazonaws.com/i/y14omvcmaqjbzz27utco.png)

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`
```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
}
```

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](https://dev.to/didof/render-props-1-grid-layout-4mo6). See you later the block of code.

#####`GridLayout.tsx`
```ts
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
```

*1. guard against the simultaneous use of children and builders*
```ts
if (typeof children === 'undefined' && typeof builder === 'undefined') {
        throw new Error('Either children or builder is required')
    }
```
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*
```ts
if (typeof builder !== 'undefined' && (typeof items === 'undefined' || items.length < 0)) {
        throw new Error('When using builder your should provide also items')
    }
```
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`*
```ts
{Boolean(children) && children!(elementWidth)}
```
Apart from a check on the existence of the method, it does nothing new. Rather, the other:

```ts
{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)}
                    />
                )
            })}
```

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()](https://reactjs.org/docs/react-api.html#reactchildren), 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 <div> 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`
```ts
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](https://dev-to-uploads.s3.amazonaws.com/i/gnx67wc5fvjipzo7m8s5.png)

> **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
* [how to make a good grid](https://css-tricks.com/snippets/css/complete-guide-grid/)
* [Making peace between ref and typescript](https://www.pluralsight.com/tech-blog/getting-size-and-position-of-an-element-in-react/#:~:text=position%20in%20React.-,Getting%20the%20Size%20and%20Position,a%20reference%20to%20that%20element.)
* [Styled-components with typescript](https://blog.agney.dev/styled-components-&-typescript/)
* [useState and typescript](https://www.carlrippon.com/typed-usestate-with-typescript/)
* [Render Props](https://medium.com/@jrwebdev/react-render-props-in-typescript-b561b00bc67c)
* [Render Props - the Return](https://blog.logrocket.com/react-reference-guide-render-props/)
* [repo](https://github.com/didof/react-render-props-grid-layout/tree/render-props-2)

### Connect with Me
GitHub - [didof](https://github.com/didof)
LinkedIn - [Francesco Di Donato](https://www.linkedin.com/in/francesco-di-donato-2a9836183/)
Twitter - [@did0f](https://twitter.com/did0f)