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;
}
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}`};
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
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;
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
}
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:
- guard against the simultaneous use of children and builders.
- forced to supply items when the builder was used.
- 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
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')
}
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')
}
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)}
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)}
/>
)
})}
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.
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
- 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)