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:
- render props
- 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[]
}
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'};
`
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:
- Props provided and where they are used
- Get the width of the component
- Calculate the size of each grid tile
- 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
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 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;
All the code in the repo/render-props-1
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>}
/>
This topic is covered in part 2
Useful links
- how to make a good grid
- Making peace between ref and typescript
- Styled-components with typescript
- useState and typescript
- Render Props
- Render Props - the Return
- repo
Connect with Me:
GitHub - didof
LinkedIn - Francesco Di Donato
Twitter - @did0f
Top comments (0)