In this tutorial, we will create a React component for a social media post editor that allows the user to write posts and display a grid of selected images while preserving their aspect ratio. The component also allows the user to remove images from the grid.
We're going to use Tailwindcss and nextjs in this tutorial so make sure to install and configure your project following Tailwind css with Nextjs guide.
To get started, let's import the necessary dependencies and create a functional component called PostEditorImages
:
import React, { useEffect, useState } from 'react';
const PostEditorImages = ({ images, removeImage }) => {
// component code goes here
}
Our component will accept two props:
images
: an array of images of typeFile
, representing the images to be displayed in the grid.removeImage
: a function that removes an image from theimages
array and updates the state of the parent component.
Next, let's set up some state variables that we will use to control the layout of the image grid:
const [imagesContainerGrid, setImagesContainerGrid] = useState("grid-cols-2 grid-rows-2");
const [aspectRatios, setAspectRatios] = useState({});
The imagesContainerGrid
state variable will hold a string of CSS class names that define a grid template columns of 2 and rows of 2. The aspectRatios
state variable will hold an object that maps the index of each image to its aspect ratio.
Now, let's write a useEffect
hook that updates the imagesContainerGrid
state variable based on the number of images:
useEffect(() => {
setImagesContainerGrid(images.length >= 3 ? "grid-cols-2 grid-rows-2 h-[300px]" : images.length > 1 ?"grid-cols-2" : "grid-cols-1");
}, [images]);
This useEffect
hook will run every time the images
prop changes. If there are three or more images, it will set the imagesContainerGrid
state variable to a string that specifies a grid with two columns and two rows, with a fixed height of 300px
. If there are two or more images, it will set the imagesContainerGrid
state variable to a string that specifies a grid with two columns. If there is only one image, it will set the imagesContainerGrid
state variable to a string that specifies a grid with one column.
Now, let's write a useEffect
hook that calculates the aspect ratio of each image and updates the aspectRatios
state variable:
useEffect(() => {
images.forEach((image, index) => {
const reader = new FileReader();
reader.onload = function(event) {
const data = event.target.result;
const img = new Image();
img.src = data;
img.onload = function() {
const gcd = (a, b) => (b === 0 ? a : gcd(b, a % b));
const numerator = img.width / gcd(img.width, img.height);
const denominator = img.height / gcd(img.width, img.height);
setAspectRatios(ratios => ({ ...ratios, [index]: `${numerator}/${denominator}` }));
};
};
reader.readAsDataURL(image);
});
}, [images]);
This code iterates over an array of images and calculates the aspect ratio of each image. It does this by:
- Creating a
FileReader
object for each image, which allows the code to read the contents of the file as a data URL. - Setting an
onload
event handler for theFileReader
object, which is called when the file has been successfully read. The event handler creates a new image object, sets itssrc
property to the data URL, and sets anonload
event handler for the image object. - The
onload
event handler for the image object calculates the aspect ratio of the image by dividing the width by the greatest common divisor of the width and height, and dividing the height by the greatest common divisor. It then updates theaspectRatios
state variable by creating a new object that spreads the existing aspect ratios and adds a new key-value pair for the current image's index and aspect ratio. - Finally, the code calls the
readAsDataURL
method on theFileReader
object, passing in the image file as an argument. This starts the process of reading the file as a data URL.
Finally, let's render the image grid in the component's JSX:
return (
<div className={` [grid-area:images] grid gap-1 ${imagesContainerGrid} `}>
{/* display the selected images */}
{images.length > 0 &&
[...images].map((image, index) => (
<div
key={image.name}
className={`relative rounded-md`}
style={{
backgroundImage: `url(${URL.createObjectURL(image)})`,
backgroundSize: 'cover',
backgroundPosition: 'center center',
backgroundRepeat: 'no-repeat',
width: '100%',
height: '100%',
aspectRatio: aspectRatios[index]
}}
>
<button
className="absolute flex items-center justify-center top-0 text-sm right-0 p-2 text-white rounded-full w-7 h-7 shadow-md bg-black bg-opacity-60 hover:bg-opacity-100"
onClick={() => removeImage(index)}
>
<span>X</span>
</button>
</div>
))
}
</div>
);
This JSX creates a div of grid-area:images
that will be defined in the parent component , with the grid
class and the imagesContainerGrid
class, which defines the layout of the image grid. Inside the div, it maps over the images
array and renders a div for each image. Each image div has a background image set to the image file, and the aspectRatio
style property set to the aspect ratio of the image. The div also has a remove button that calls the removeImage
function and passes in the index of the image to be removed.
That's it! With these simple steps, we have created a React component that displays a grid of images selected by the user, and allows the user to remove images from the grid.
To use the component, we need to create the SocialMediaTextEditor
component which represents the text editor for the the social media app:
const SocialMediaTextEditor = () => {
const [text, setText] = useState('')// state to store the text;
const [images, setImages] = useState([]); // state to store the images
const imageContainerRef = useRef()
const handleTextChange = (event) => {
setText(event.target.value);
};
const handleImageChange = (event) => {
// update the images state by concatenating the selected files with the existing ones
setImages([...images, ...event.target.files]);
};
const removeImage = (index) => {
// create a new array with the selected image removed
const newImages = [...images];
newImages.splice(index, 1);
setImages(newImages);
};
return (...)
Here, we have the handleTextChange
to save the latest value of the text field, the handleImageChange
and removeImage
functions are responsible for adding and removing images from the images list.
Now lets define and style the post editor component using grid template areas with tailwindcss:
return (
<div
className={` rounded-lg p-4
grid gap-y-1
grid-cols-[40px_5px_1fr]
grid-rows-[40px_1fr_minmax(0,_max-content)_40px]
[grid-template-areas:'profile_p1_post'_'empty2_p3_post'_'empty3_p3_images'_'empty4_p4_postActions']`}
>
{/* */}
</div>
)
The SocialMediaTextEditor
component contains a profile area where you can add a profile image. The post
area represents the text field where the user can write their post. The p1
, p3
, and p4
represent a 5px
gap that is defined in the grid-cols-[40px_5px_1fr]
layout. The postActions
area holds the upload images button and the post button. The emptyX
fills in the profile area in all grid rows except the first row, giving the component a similar style to the Twitter post editor.
Now, let's define the JSX code for the profile, text field, and displayed images:
...
ā {/* profile area */}
<div className="[grid-area:profile] rounded-full bg-red-500" />
{/* post area */}
<textarea
className="w-full [resize:none] [grid-area:post] h-fit p-2 rounded-lg text-gray-500 focus:outline-none"
placeholder="Write your post here..."
value={text}
onChange={handleChange}
/>
{/* display selected images */}
<PostEditorImages images={images} removeImage={removeImage} />
ā
...
Now add the jsx code that will allow us to select images from the local system :
...
{/* display selected images */}
<PostEditorImages images={images} removeImage={removeImage} />
<div className="[grid-area:postActions] flex flex-row justify-between">
<div className="justify-self-start flex flex-row gap-2">
{/* add the input element to select images */}
<label htmlFor="image-upload" className="cursor-pointer">
<FaImage size={20} color="rgb(31 41 55)" />
</label>
<input
id="image-upload"
type="file"
accept="image/*"
multiple
onChange={handleImageChange}
className="hidden"
/>
</div>
<button
className="bg-red-500 justify-self-end hover:bg-red-700 text-white font-bold py-2 px-4 rounded-full"
>
Post
</button>
</div>
ā
...
You can import the FaImage
component from react-icons
package.
That's it! You now have a fully functional social media post editor component with a responsive display images grid and a style similar to the Twitter post editor. Of course, you still need to create the logic for uploading the images to the server.
I hope this tutorial was helpful in showing you how to display and manage a grid of images in a React app. Happy coding!