DEV Community

Cover image for React Render Props with Ts & styled-components | Part 1
Francesco Di Donato
Francesco Di Donato

Posted on • Edited on

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

What are we building

A React component that allows the setting up of a grid. For more flexibility, you will have two different methods for rendering the content:

  1. render props
  2. builder method (part 2)

Quick setting up

  • npx create-react-app [name] --template typescript
  • Cleaning the boilerplate
  • Install styled-components & @types/styled-components

or clone from repo/boilerplate

What we need

We need a component that can be instructed on the number of columns. It will be able to derive the width it occupies inside the DOM and with a simple division it will draw the width (pixel) assigned to each child. This last information will be displayed through the render props and can be used immediately with great flexibility.

Method

Create in src/components/GridLayout

  • GridLayout.types.ts
  • GridLayout.styles.ts
  • GridLayout.tsx

The component categorically needs the number of columns. If necessary, it also allows you to set the minimum height of each grid tile. Above all, it not only wraps the children but injects an intermediate step in the form of a function that exposes the width of each tile.

GridLayout.types.ts
export interface GridLayoutProps {
    columnsAmount: number;
    rowHeight?: number;
    children: (width: number) => JSX.Element | JSX.Element[]
}
Enter fullscreen mode Exit fullscreen mode

Now we define the styled component that will be used in the component. For simplicity here the styling is done all in one go - however it is easy to imagine a streamlining thanks to advanced features.

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

type GridProps = {
    columnsAmount: number;
    rowHeight?: number;
}

export const Grid = styled.div <GridProps>`
    width: 100%;
    height: 100%;

    display: grid;

    justify-content: center;
    align-items: center;

    grid-template-columns: ${({ columnsAmount }) => `repeat(${columnsAmount}, 1fr)`};
    grid-auto-rows: ${({ rowHeight }) => rowHeight ? `${rowHeight}px` : 'auto'};

`
Enter fullscreen mode Exit fullscreen mode

Basically, it is a grid, which places the various children each in the center of their own piece. The number of columns is decided from the outside. Same thing happens for the size of each row, with the difference of a fallback on auto in case of no definition.


It's time to put the pieces together and build the magic component. It may seem overwhelming at first glance but let's break it down like this:

  1. Props provided and where they are used
  2. Get the width of the component
  3. Calculate the size of each grid tile
  4. Render Props

See you after the code block

GridLayout.tsx
import { useState, useEffect, createRef } from 'react'
import { GridLayoutProps } from './GridLayout.types'

import { Grid } from './GridLayout.styles'

const GridLayout = ({ children, columnsAmount, rowHeight }: GridLayoutProps): JSX.Element => {

    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}
        >
            {children(elementWidth)}
        </Grid>
    )
}

export default GridLayout

Enter fullscreen mode Exit fullscreen mode

1. Props provided and where they are used

It receives the props defined in GridLayout.types.ts.
Putting children aside for a moment, columnsAmount and rowHeight are both passed to <Grid> (I mean the styled component). And here the grid is done. Now let's fill it.

2. Get the width of the component

This point is slightly more tricky. We need to somehow access the width of the component. So we associate a ref to the outermost component ().

Why the signature on createRef()? Explained here

However, the current property of the ref, i.e. the DOM node itself, is undefined until the component has been mounted by React.
For this we move to useEffect(..., [...dependencies]). It runs after the component has been mounted, so we can access the current. We don't stop there, but we use the getBoundingClientRect() method and extract the width of the component from the result. Let's not forget to save it in a variable.

For the sake of simplicity, in case of window resizing, the value is not updated and the result (as you will be able to experience shortly) will be mined. I leave you the pleasure of solving it and this nice hook. Be careful not to prick yourself.

3. Calculate the size of each grid tile

We have the width of the container
+
we know how many equal pieces to split it
=
how wide each piece is.

Let's save the result of this division (via useState, I recommend) somewhere.

4. Render Props

Here we are again at the prop not yet used, children. Remember it's a function. So where we would have simply done {children}, this time do {children ()}. And typescript gives us error - it remember that we have instructed it that it is a function that requires a parameter of type number. And here's where we put the last piece of the puzzle {children (elementWidth)}.

Epilogue

We import GridLayout in App.tsx. Let's build a list of colors (in this case only 2, alternating). Finally, we use GridLayout, telling it we want 8 columns. Inside we invoke the function that gives us the width of each element that we can use at will.

import React from 'react'

import GridLayout from './components/GridLayout/GridLayout'

const colors = ['black', 'brown']

const itemList = (amount: number, shift: number): string[] => {
  let i = amount;
  let output = [];
  let toggle = 0;
  while (i > 0) {
    if(i % shift !== 0) toggle = Boolean(toggle) ? 0 : 1
    output.push(colors[toggle])
    i--;
  }
  return output;
}

const App = () => (
  <div>
    <h1>React Render Props Grid Layout</h1>
    <GridLayout
      columnsAmount={8}
    >
      {(itemWidth) => React.Children.map(itemList(64, 8), color => (
        <div
          style={{
            width: itemWidth,
            height: itemWidth,
            backgroundColor: color
          }} />
      ))}
    </GridLayout>
  </div>
)

export default App;
Enter fullscreen mode Exit fullscreen mode

All the code in the repo/render-props-1

Alt Text


Rather than returning a bland div, go wild. Also, try to change the window size and refresh the page.

Why children? Because it is special and allows you to do this thing to open braces and start writing inside a component.
If you had chosen another name - for example builder(?) - you would have had to invoke the function as the value of a prop:

// example
<GridLayout
   builder={(foo) => <div>{foo}</div>}
/>
Enter fullscreen mode Exit fullscreen mode

This topic is covered in part 2


Useful links


Connect with Me:

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

Top comments (0)