Let's build a matching-card game with React

Nguyễn Quyết
Aug 7 · 5 min read

1 — The idea

We have 16 cards in total, but we need to have a pair of each image to match so we need 8 different type of card The value of 8 type are:

Each card should have a form like this

The flow when we click on each card is:

When 1 card open, let it there, when 2 cards open, compare it, if it’s the same type then keep it always open , if not close both. So the idea is, create an array named checkers to store 2 open card and compare, and another array named completed to store the type of cards that match in checkers array.

The card only open when it’s in checkers or its type belong to completed array. The game is done when completed array contains all 8 types.

As you can see on the picture above, because card type css done, so completed contains the value css. And when we’re open 2 other cards rail and vue to compare, checkers array hold 2 card object.

2 Let's code

Im using create-react-app to create a simple React app. And here is code for our app:

/** src/App.js */
import React, {useState} from 'react'
function App(){
const [cards, setCards] = useState(buildCards())
const renderCard = card => {
const {frontImg, backImg, flipped} = card
const img = flipped ? frontImg : backImg
return (
<div className="Card">
<img src={img} alt=""/>
</div>
)
}

return <div className="App">
<div className="Board">
{cards.map(renderCard)}
</div>
</div>
}

We replaced the default content of create-react-app with the code above, it’s just a React component that render a bunch of <div class="Card"/> inside <div class="Board"/> and <div class="App"/> As you can see there is a function buildCards not implemented.

We want to render 16 cards to screen, so buildCards function must return an array of 16 item object in this shape

And this is how buildCards implemented

function buildCards(){
let id = 0
const images = {
angular: 'url to img',
css: 'url to img',
html: 'url to img',
go: 'url to img',
rail: 'url to img',
react: 'url to img',
scala: 'url to img',
vue: 'url to img',
}
const cards = Object.keys(images).reduce((result,key) => {
const createCard = () => ({
id: id++,
type: key,
backImg: 'url to backImg',
frontImg: images[key],
flipped: false
})
result.push(createCard())
result.push(createCard())
return result
}, [])
return cards
}

Let explain what is going on on buildCards function. We first have an images object that contain 8 different keys, why 8? Because with each key, we will create 2 card object, so we will have 16 in total. Next we use reduce function to iterate though every key of that object, and with each key, we call result.push(createCard()) 2 times to create 2 objects.

The reason to create a function createCard is help to generate different unique id for each item created.

NOTE We created 16 objects and every 2 objects have the same type but they have their own id. And the card will close by default so flipped set to false

This is what we got after generate 16 cards on screen

Let open all cards by changing flipped: true in createCard function

Done. BUTTTTTT, something is not right here, if the Card is rendered like this, game is too easy when 2 card with the same type rendered right next to each others.
Solution: create a function name suffer take in an array and return another array with order of its items randomized.

function suffer(arr) {
for(let i = 0; i > arr.length; i++){
let randomIdx = Math.floor(Math.random() * arr.length)
let copyCurrent = {...arr[i]}
let copyRandom = {...arr[randomIdx]}
arr[i] = copyRandom
arr[randomIdx] = copyCurrent
}
return arr
}

This function just do a loop through an array passed in parameter, then replace each item with random item in array, so we will get back an array with index of each item randomized. Now let make a small change in buildCards function. Change the last line return cards to return suffer(cards) . And this is what we got after doing this.

Niceeeee. We’re half ways done. Next, we will handle onClick for Card with the flow I was mentioned above. The complete code of App.js will look like this.

The main flow of code from line 21 to line 30 . I tried my best to describe the flow, hope I don’t need re-explain it. All 4 functions at the bottom is just pure function that help us write better readable code. I write it in pure function to make it easy to test later and avoid side effect.

Note: You might have wonder about newCheckers and checkers about why and when to use which. React setState run asynchronously so after call setCheckers(newCheckers) the state checkers is not changed immediately, so accessing it right after call setCheckers will not correct, that’s why after call setCheckers we use newCheckers instead of checkers . And about resetChecker after 1 second, it’s because we have to leave that time for user to see two cards is currently showing and they can remember card position, if we’re not setTimeout 1 second, user will not have change to see both 2 cards open if they’re not matched. Try to reduce the timer so you will see the different.

We have implemented code for onCardClick function. The state checkers and completed are now changed according to user click on card. But screen is not re-render because it’s only render when state cards changed. So the final step we need to make the cards changed when checkers or completed changed

useEffect(()=>{
const newCards = cards.map(card => ({
...card,
flipped: checkers.find(c => c.id === card.id) ||
completed.includes(card.type)
}))
setCards(newCards)
}, [checkers, completed])

DONE. In the last step, we update the card flipped property based on checkers and completed .

The completed code can be found at `https://github.com/chiquyet199/memory-game` . It might look a bit different in term of file structure and components. But the flow and logic is exactly the same.

Nguyễn Quyết

Written by

javascrip lover

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade