Table of Contents
The best way to learn something is by doing. It works even better if it means working on something for fun. So, how about learning about React, React hooks and TypeScript by building your own meme generator? This tutorial will show you how to do it.
You can find the code on my GitHub.
Briefing
This meme generator will allow you to generate png or jpg image from HTML content. This content can be anything you want. For this project, it will be a single image and two headings, positioned absolutely on the image. The first heading will be at the top of the image and the second will be at the bottom.
You will be able to add the image in two ways. First, the meme generator will fetch random image from api.imgflip.com
. Don’t worry, no token or registration required. Second, you will be able to open image from your disk, using file
input. To generate the png or jpg file this meme generator will use dom-to-image-more
package.
About the code. This tutorial will use React hooks such as useState
, useEffect
and useRefs
. Since you will use hooks there is no need for class components. So, you will build all components for your meme generator as functional components. You will write this meme generator in TypeScript and you will also work with interfaces
and types
.
Project setup
Let’s set up the files you will need to build your meme generator. You can do this very quickly by using create-react-app as your starting template. If you want, you can install this package globally on your computer, with your favorite package manager (pnpm, yarn or npm). However, this is not really necessary.
You can also create the starting template without installing anything. This can be done either with npx, instead of npm, or pnpx, instead of pnpm. These two commands will download the desired package, install it temporarily, automatically start it, and remove it after you are done. No need to fill your HDD.
One more thing, you will write this meme generator in TypeScript, a superset of JavaScript. If you want to create the starter template with create-react-app with support for TypeScript you have to include --typescript
flag in the command. If you don’t want to use TypeScript in this project, omit the --typescript
flag.
To the installation. For npx, use npx create-react-app react-meme-generator-ts --typescript
. You can also use npm directly, npm init react-meme-generator-ts --typescript
. For pnpx, it will be npx create-react-app react-meme-generator-ts --typescript
. For yarn, use yarn create react-app react-meme-generator-ts --typescript
.
These commands will create a starter template for your meme generator. Now, let’s also add the dom-to-image-more
package. When you are done with this, you are ready to start. Your package.json
will look something like this:
{
"name": "react-meme-generator-ts",
"version": "1.0.0",
"description": "Meme generator web app built with React, React hooks and TypeScript.",
"license": "MIT",
"private": false,
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
],
"main": "src/index.tsx",
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
"dependencies": {
"dom-to-image-more": "2.8.0",
"react": "16.11.0",
"react-dom": "16.11.0",
"react-scripts": "3.2.0"
},
"devDependencies": {
"@types/react": "16.9.11",
"@types/react-dom": "16.9.3",
"typescript": "3.6.4"
}
}
One thing. Below is the final structure of the meme generator you are going to build. You can use this to help yourself orient in the code.
react-meme-generator-ts/
├─node_modules
├─public
│ ├─favicon.ico
│ ├─index.html
│ ├─manifest.json
│ └─robots.txt
├─src
│ ├─components
│ │ ├─content.tsx
│ │ ├─form.tsx
│ │ └─result.tsx
│ ├─styles
│ │ └─styles.css
│ ├─index.tsx
│ └─react-app-env.d.ts
├─ package.json
└─ tsconfig.json
Form component
The first component you will build will be a form. To be specific, it will actually be a div
with couple of input
elements and buttons. There will be two inputs, one for text at the top and one for the text on the bottom. Next, there four buttons, one for generating real png image of the meme.
Second button will change the image, load random image provided by api.imgflip.com
. Third button will allow you to upload your own image from your disk. This will button will actually be file
input wrapped inside label
element. The fourth button will reset the image, i.e. remove the generated meme from DOM.
About the “Reset” button. The meme generator will display this button only when some meme image is generated. Otherwise, this button component will not exist in the DOM.
// Import react
import * as React from 'react'
// Interface for Form Component
interface FormInterface {
isMemeGenerated: boolean;
textBottom: string;
textTop: string;
handleImageChange: () => void;
handleImageInputChange: (event: React.ChangeEvent) => void;
handleInputChange: (event: React.ChangeEvent) => void;
handleMemeGeneration: () => void;
handleMemeReset: () => void;
}
// Form component
const Form = (props: FormInterface) => {
return (
<div className="form">
<div className="form__inputs">
{/* Input for the text at the top */}
<input
name="text-top"
placeholder="Text top"
type="text"
value={props.textTop}
onChange={props.handleInputChange}
/>
{/* Input for the text at the bottom */}
<input
name="text-bottom"
placeholder="Text bottom"
type="text"
value={props.textBottom}
onChange={props.handleInputChange}
/>
</div>
<div className="form__btns">
{/* Button to load random image from api.imgflip.com */}
<button
className="btn btn-primary"
type="button"
onClick={props.handleImageChange}
>
Change image
</button>
{/* 'Button' to load image from disk */}
<label
className="btn btn-primary"
htmlFor="fileInput"
>
Load image
<input id="fileInput" name="fileInput" type="file" accept=".jpg, .jpeg, .png" onChange={props.handleImageInputChange} hidden />
</label>
{/* Button to generate png image of the meme */}
<button
className="btn btn-primary"
type="button"
onClick={props.handleMemeGeneration}
>
Generate meme
</button>
{/* Button to remove the meme image from the DOM */}
{props.isMemeGenerated && <button
className="btn btn-danger"
type="button"
onClick={props.handleMemeReset}
>
Reset
</button>}
</div>
</div>
)
}
export default Form
Content component
The Content
component will be very simple. There will be one wrapper div
with img
element to preview the meme image, and h1
for the text at the top and h2
for the text at the bottom. The wrapper div
will have a ref
.
You will use this ref later to make it easier to reference this div
, and generate the meme from its HTML content. That’s it for the Content
component.
// Import react
import * as React from 'react'
// Interface for Content component
interface ContentInterface {
activeImage: string;
contentContainerRef: React.RefObject<any>;
textBottom: string;
textTop: string;
}
// Content component
const Content = (props: ContentInterface) => {
return (
<div className="content" ref={props.contentContainerRef}>
{/* Image preview */}
<img src={props.activeImage} alt="Meme" />
{/* Text at the top */}
<h1>{props.textTop}</h1>
{/* Text at the bottom */}
<h2>{props.textBottom}</h2>
</div>
)
}
export default Content
Result component
The third component you will build will be the Result
component. This component will be a div
that will wrap the png or jpeg image, this meme generator will create. The wrapper div
will also have a ref
. You will use this ref
to append the newly generated meme image, and also to remove any existing when you click the “Reset” button.
// Import react
import * as React from 'react'
// Interface for Result component
interface ResultInterface {
resultContainerRef: React.RefObject<any>;
}
// Result component
const Result = (props: ResultInterface) => {
return (
<div ref={props.resultContainerRef} className="result"></div>
)
}
export default Result
Main (index) component
It is time for the fourth and most important and complex component. This component will render all smaller components you’ve built so far. It will also provide them with logic and functionality. So, when you finish this component your meme generator be ready to use. Well, almost. It will need some styles. But now, the main component.
Imports
As the first thing, you will need to import react
, react-dom
and dom-to-image-more
packages. Next, you will also need to import all components you’ve built so far, i.e. Content
, Form
and Result
. Then, you can add import for CSS stylesheet so you can later add some CSS styles to style your meme generator.
Refs and states
At the top of the main App
component, you will create refs for the content and result div
elements, contentContainerRef
and resultContainerRef
, using useRef
React hook. Next, you will add states for images fetched from API, active image, top and bottom texts and for boolean isMemeGenerated. All with React useState
React hook.
function App() {
// Create refs
let contentContainerRef = React.useRef<HTMLElement | null>(null)
let resultContainerRef = React.useRef<HTMLElement | null>(null)
// Create useState hooks
const [images, setImages] = React.useState([])
const [activeImage, setActiveImage] = React.useState('')
const [textTop, setTextTop] = React.useState('')
const [textBottom, setTextBottom] = React.useState('')
const [isMemeGenerated, setIsMemeGenerated] = React.useState(false)
// ...
}
Fetching the API
Then will come the first method, fetchImage. This method will be async. It will use fetch
method to fetch the data from api.imgflip.com
endpoint. The result will be an array of images with some additional information. You will store this array in images
state using the setImages
React hook.
After that you will take the first image in the array and set it as active image, i.e. store it in activeImage
state, using the setActiveImage
.
// ...
// Fetch images from https://api.imgflip.com/get_memes
async function fetchImage() {
// Get the memes
const imgData = await fetch('https://api.imgflip.com/get_memes').then(res => res.json()).catch(err => console.error(err))
const { memes } = await imgData.data
// Update images state
await setImages(memes)
// Update activeImage state
await setActiveImage(memes[0].url)
}
// ...
Handling the text inputs
Second method will be handleInputChange
. You will use this method to handle inputs for meme image texts, the top and bottom. You will use event.target.name
and if
statement to detect which text is firing the event. Then, you will change the textTop
, or textBottom
, state using the setTextTop
, or setTextBottom
, React hook.
You will use event.target.value
to extract the text from the input, and pass it to the state.
// ...
// Handle input elements
function handleInputChange(event) {
if (event.target.name === 'text-top') {
// Update textTop state
setTextTop(event.target.value)
} else {
// Update textBottom state
setTextBottom(event.target.value)
}
}
// ...
Handling the image change
The third method will be handleImageChange
. This method will be initiated by clicking on the “Reset” button. It will take the array of images stored in images
state, generate random number, and use that number as an index to choose one random image from the array.
// ...
// Choose random images from images fetched from api.imgflip.com
function handleImageChange() {
// Choose random image
const image = images[Math.floor(Math.random() * images.length)]
// Update activeImage state
setActiveImage(image.url)
}
// ...
Handling the file input
The fourth method will be handleImageInputChange
. This method will load the file loaded via the file input and use the setActiveImage
React hook to change the activeImage
state to the URL created for the image file you’ve uploaded from your disk.
// ...
// Handle image upload via file input
function handleImageInputChange(event) {
// Update activeImage state
setActiveImage(window.URL.createObjectURL(event.target.files[0]))
}
// ...
Generating the meme image
The fifth method will be handleMemeGeneration
. First, you will create a condition to check for any childNodes
inside the result container. If there is child node, this method will remove it. Otherwise, it will proceed to generating the meme image. This will make it sure there is always only one rendered image.
The generator will generate the image in png format, using the domtoimage
package and its toPng
method. You can also use jpg (with toJpeg
) or svg (with toSvg
) formats. Next, you will pass the contentContainerRef.current
as argument to the toPng
method, to find the content container where you want to render the meme image.
After that, you will create new image element, use URL of the generated image as src
and append this new image to DOM, using the resultContainerRef
. When this is done, you will change isMemeGenerated
state to true
using the setIsMemeGenerated
React hook. This will tell React to display the “Reset” button.
// ...
// Handle meme generation
function handleMemeGeneration() {
// Remove any existing images
if (resultContainerRef.current.childNodes.length > 0) {
resultContainerRef.current.removeChild(resultContainerRef.current.childNodes[0])
}
// Generate meme image from the content of 'content' div
domtoimage.toPng(contentContainerRef.current).then((dataUrl) => {
// Create new image
const img = new Image()
// Use url of the generated image as src
img.src = dataUrl
// Append new image to DOM
resultContainerRef.current.appendChild(img)
// Update state for isMemeGenerated
setIsMemeGenerated(true)
})
}
// ...
The sixth method you will create is handleMemeReset
. This method will remove existing child node inside result container, generated meme image. Then, it will set the isMemeGenerated
state to false
using the setIsMemeGenerated
React hook. This will tell React to remove the “Reset” button.
// ...
// Handle resetting the meme generator/removing existing pictures
function handleMemeReset() {
// Remove existing child node inside result container (generated meme image)
resultContainerRef.current.removeChild(resultContainerRef.current.childNodes[0])
// Update state for isMemeGenerated
setIsMemeGenerated(false)
}
// ...
Combining fetchImage with useEffect
Almost the last step. You will combine useEffect
React hook with fetchImage
method. This will cause that when the app mounts it will automatically fetch images from the API and set the first one as active. And, you will render the App
component in the DOM.
// ...
// Fetch images from https://api.imgflip.com/get_memes when app mounts
React.useEffect(() => {
// Call fetchImage method
fetchImage()
}, [])
// ...
Returning all components
The last step. Now, you will take all the components you’ve built, and imported, and add them to the main App
component.
// ...
return (
<div className="App">
{/* Add Form component */}
<Form
textTop={textTop}
textBottom={textBottom}
handleImageInputChange={handleImageInputChange}
handleInputChange={handleInputChange}
handleImageChange={handleImageChange}
handleMemeGeneration={handleMemeGeneration}
handleMemeReset={handleMemeReset}
isMemeGenerated={isMemeGenerated}
/>
{/* Add Content component */}
<Content
activeImage={activeImage}
contentContainerRef={contentContainerRef}
textBottom={textBottom}
textTop={textTop}
/>
{/* Add Result component */}
<Result resultContainerRef={resultContainerRef} />
</div>
)
}
// Render the App in the DOM
const rootElement = document.getElementById('root')
render(<App />, rootElement)
Putting it all together
Now, let’s put all the pieces for the App
component together.
// Import react, react-dom & dom-to-image-more
import * as React from 'react'
import { render } from 'react-dom'
import domtoimage from 'dom-to-image-more'
// Import components
import Content from './components/content'
import Form from './components/form'
import Result from './components/result'
// Import styles
import './styles/styles.css'
// App component
function App() {
// Create refs
let contentContainerRef = React.useRef<HTMLElement | null>(null)
let resultContainerRef = React.useRef<HTMLElement | null>(null)
// Create useState hooks
const [images, setImages] = React.useState([])
const [activeImage, setActiveImage] = React.useState('')
const [textTop, setTextTop] = React.useState('')
const [textBottom, setTextBottom] = React.useState('')
const [isMemeGenerated, setIsMemeGenerated] = React.useState(false)
// Fetch images from https://api.imgflip.com/get_memes
async function fetchImage() {
// Get the memes
const imgData = await fetch('https://api.imgflip.com/get_memes').then(res => res.json()).catch(err => console.error(err))
const { memes } = await imgData.data
// Update images state
await setImages(memes)
// Update activeImage state
await setActiveImage(memes[0].url)
}
// Handle input elements
function handleInputChange(event) {
if (event.target.name === 'text-top') {
// Update textTop state
setTextTop(event.target.value)
} else {
// Update textBottom state
setTextBottom(event.target.value)
}
}
// Choose random images from images fetched from api.imgflip.com
function handleImageChange() {
// Choose random image
const image = images[Math.floor(Math.random() * images.length)]
// Update activeImage state
setActiveImage(image.url)
}
// Handle image upload via file input
function handleImageInputChange(event) {
// Update activeImage state
setActiveImage(window.URL.createObjectURL(event.target.files[0]))
}
// Handle meme generation
function handleMemeGeneration() {
// Remove any existing images
if (resultContainerRef.current.childNodes.length > 0) {
resultContainerRef.current.removeChild(resultContainerRef.current.childNodes[0])
}
// Generate meme image from the content of 'content' div
domtoimage.toPng(contentContainerRef.current).then((dataUrl) => {
// Create new image
const img = new Image()
// Use url of the generated image as src
img.src = dataUrl
// Append new image to DOM
resultContainerRef.current.appendChild(img)
// Update state for isMemeGenerated
setIsMemeGenerated(true)
})
}
// Handle resetting the meme generator/removing existing pictures
function handleMemeReset() {
// Remove existing child node inside result container (generated meme image)
resultContainerRef.current.removeChild(resultContainerRef.current.childNodes[0])
// Update state for isMemeGenerated
setIsMemeGenerated(false)
}
// Fetch images from https://api.imgflip.com/get_memes when app mounts
React.useEffect(() => {
// Call fetchImage method
fetchImage()
}, [])
return (
<div className="App">
{/* Add Form component */}
<Form
textTop={textTop}
textBottom={textBottom}
handleImageInputChange={handleImageInputChange}
handleInputChange={handleInputChange}
handleImageChange={handleImageChange}
handleMemeGeneration={handleMemeGeneration}
handleMemeReset={handleMemeReset}
isMemeGenerated={isMemeGenerated}
/>
{/* Add Content component */}
<Content
activeImage={activeImage}
contentContainerRef={contentContainerRef}
textBottom={textBottom}
textTop={textTop}
/>
{/* Add Result component */}
<Result resultContainerRef={resultContainerRef} />
</div>
)
}
// Render the App in the DOM
const rootElement = document.getElementById('root')
render(<App />, rootElement)
Styles
Your meme generator is almost ready. The last thing you can do is adding some styles to make it look better.
/* Default styles */
html {
box-sizing: border-box;
font-size: 16px;
}
*,
*::before,
*::after {
box-sizing: inherit;
}
body {
margin: 0;
font: 1rem sans-serif;
}
/* App */
.App {
text-align: center;
}
/* Content */
.content {
position: relative;
display: flex;
align-items: center;
flex-flow: column;
justify-content: center;
margin-top: 16px;
}
img {
max-width: 520px;
height: auto;
max-height: 500px;
object-fit: contain;
}
h1,
h2 {
position: absolute;
margin: 0;
width: 100%;
font-family: Impact, Haettenschweiler, 'Arial Narrow Bold', sans-serif;
font-size: 48px;
text-align: center;
text-transform: uppercase;
color: #fff;
/* text-shadow: 0px 0px 2px black; */
-webkit-text-stroke: 3px black;
line-height: 1;
}
h1 {
top: 16px;
}
h2 {
bottom: 32px;
}
/* Form */
.form {
margin: 0 auto;
max-width: 380px;
}
.form__inputs,
.form__btns {
display: flex;
flex-flow: row nowrap;
}
.form__inputs {
margin-bottom: 12px;
}
.form__inputs input,
.form__btns .btn {
border-radius: 2px;
}
.form__inputs input {
padding: 8px;
width: 100%;
max-width: 50%;
border: 1px solid #ccc;
}
.form__inputs input:focus {
outline-color: #0984e3;
}
.form__inputs input + input,
.form__btns .btn + .btn {
margin-left: 12px;
}
.form__btns {
justify-content: center;
}
.form__btns .btn {
padding: 8px 12px;
border: 0;
cursor: pointer;
color: #fff;
transition: background .25s ease-in-out;
}
/* Buttons */
.btn-primary {
background: #0984e3;
}
.btn-primary:hover {
background: #0767b2;
}
.btn-danger {
background: #d63031;
}
.btn-danger:hover {
background: #b02324;
}
Conclusion: Build your own Meme Generator…
Good job! You’ve just built your own meme generator with React, React hooks and TypeScript. I hope you’ve enjoyed this tutorial and learned something new, something you can use in your future project. Next steps? Find a way to make this meme generator better. Add new features you would like it to have. Your imagination is the only limit. Have fun.
If you liked this article, please subscribe so you don't miss any future post.
If you'd like to support me and this blog, you can become a patron, or you can buy me a coffee 🙂