Sehari sebelum tulisan ini dibuat, saya kembali bertemu sebuah error yang pernah saya dapatkan ketika mengembangkan salah satu toy project saya yaitu anime book. Error ini memang belum terpecahkan dari dulu sampai kemarin, atau sebelum tulisan ini dibuat.
Dulu alasannya adalah karena toy project yang saya kerjakan dulu tidak akan digunakan oleh banyak orang, jadi saya cari cara lain supaya error tersebut tidak muncul dan bukan diatasi yang akhirnya membuat pengembangannya menjadi tidak maksimal bahkan terhenti sampai sekarang. Tapi bakal saya lanjutkan lagi dan rencananya mau sekalian saya ubah di bagian tampilannya.
Tapi project yang sekarang itu awalnya adalah salah satu syarat masuk dari sebuah perusahaan e-commerce ternama yang logonya ijo. Ya, mereka tertarik dengan CV yang saya buat dan mengirimkan sebuah tugas untuk dikerjakan. Namun setelah membaca requirement dan tools yang harus digunakan, saya memilih untuk menurunkan ekspektasi untuk masuk kesana karna kebanyakan toolsnya belum pernah saya pakai bahkan beberapa ada yang asing bagi saya hahaha.
Saya tetap mengerjakan tugas tersebut sembari belajar tentang requirement dan toolsnya, lumayan bisa buat tambah-tambah portofolio karna memang tugasnya lumayan kompleks.
Dan disinilah saya bertemu lagi dengan error cannot read map of undefined ini. Kali ini saya coba untuk solve error tersebut dan akhirnya solved!. Jadi saya coba dokumentasikan di tulisan ini.
Masalah
Untuk dapat memahami apa solusi yang dapat digunakan, pertama-tama mari coba pahami masalahnya yaitu kenapa error tersebut bisa muncul.
Disini saya akan membuat sebuah contoh blok kode.
const pokeapiUrl = "https://pokeapi.co/api/v2/pokemon/spearow";
const getData = async () => await fetch(pokeapiUrl).then(res => res.json());
function App() {
const [pokemonData, getPokemonData] = useState();
useEffect(() => {
getData().then(data => getPokemonData(data));
}, []);
return(
<div>
{pokemonData.types.map(pokemon => (
<div key={pokemon.type.name}>{pokemon.type.name}</div>
))}
<div>
)
}
export default App;
Disini kita punya sebuah komponen yang mengatur sebuah state dari data pokemon dan komponen tersebut juga memiliki operasi useEffect dimana kita menjalankan sebuah proses asinkronus didalamnya, yaitu getData(). proses ini akan mengembalikan data yang kita perlukan dari API yang sedang kita pakai.
Komponen ini juga me-render data tersebut dan terdapat proses iterasi di dalam data tersebut dengan menggunakan .map yang mana mengembalikan sebuah react element untuk setiap data.
Lalu kita tekan ctrl + s untuk menyimpan perubahan yang dibuat dan setelah itu membuka browser untuk melihat hasilnya.
Apa yang muncul?
Yak, bukannya tampilan sebuah list tipe dari pokemon spearow melainkan sebuah error yaitu TypeError: Cannot read property ‘map’ of undefined
Apa sebenarnya yang terjadi?
Kita memiliki variabel pokemonData:
const [pokemonData, getPokemonData] = useState();
Lalu kita populate variabel tersebut dengan data yang didapatkan dari API:
useEffect(() => {
getData().then(data => getPokemonData(data));
}, []);
Oke, mari kita cek bagaimana flow dari react yang terdapat pada contoh diatas.
- React me-render atau meng-invoke komponen yang sudah dibuat
- React meng-eksekusi useState call dan mengembalikan kepada kita
[undefined, fn]
- React meng-evaluasi return statement yang kita buat, ketika sampai pada baris
items.map(...)
sebenarnya React menjalankanundefined.map(...)
yang mana sudah jelas akan menghasilkan sebuah error di bahasa pemrograman JavaScript
Bagaimana dengan call useEffect nya?
React akan menjalankan semua effect atau useEffect-nya setelah me-render komponen di browser, yang mana kita tidak dapat menghindari sebuah first render atau render saat halaman pertama kali dibuat tanpa data yang kita ambil dari API.
Solusi yang bisa diterapkan
1. Initial Value
Solusi pertama yang bisa diterapkan adalah memberi variabel kita sebuah default initial value, yang dengan useState akan terlihat seperti berikut ini:
const [pokemonData, getPokemonData] = useState([]);
Ini berarti ketika react menjalankan useState([])
call kita, maka hasilnya akan mengembalikan ini
[[], fn];
Yang mana saat terjadi first render di komponen kita, react akan melihat data kita sebagai sebuah empty array atau array kosong. Jadi react tidak akan menjalankan undefined.map(...)
seperti pada penjelasan sebelumnya, melainkan react akan menjalankan [].map(...)
.
2. Conditional Rendering
Solusi kedua yang bisa diterapkan adalah dengan me-render secara kondisional atau conditional rendering komponen yang dibuat berdasarkan data yang didapat dari API. Ini artinya ketika kita sudah mendapatkan datanya, maka akan me-render komponen tersebut dengan data yang didapat, dan akan me-render elemen lain atau pengganti ketika datanya sedang diambil (misalnya tampilan Loading).
Ketika menggunakan JSX, kita tidak disarankan atau malah tidak diperbolehkan membuat if-else ke dalam return statement kita. Tapi kita bisa membuatnya diluar return statement, kurang lebih seperti ini:
function App() {
const [pokemonData, getPokemonData] = useState();
useEffect(() => {
getData().then(data => getPokemonData(data));
}, []);
let itemsToRender;
if(pokemonData) {
itemsToRender = pokemonData.types.map(pokemon => {
<div key={pokemon.type.name}>{pokemon.type.name}</div>
});
}
return(
<div>{itemsToRender}<div>
)
}
Value undefined atau null akan di-ignored di dalam konteks JSX jadi aman untuk di pass ke first render.
Kita juga dapat menambahkan else statement jika kita ingin me-render elemen lain seperti loading text atau spinner:
function App() {
const [pokemonData, getPokemonData] = useState();
useEffect(() => {
getData().then(data => getPokemonData(data));
}, []);
let itemsToRender;
if(pokemonData) {
itemsToRender = pokemonData.types.map(pokemon => {
<div key={pokemon.type.name}>{pokemon.type.name}</div>
});
} else {
itemsToRender = "Loading...";
}
return(
<div>{itemsToRender}<div>
)
}
3. Inline Conditional Rendering
Cara terakhir yang bisa diterapkan adalah dengan membuat conditional rendering di dalam return statement. Lho, tadi katanya ga bisa? kok ada caranya ini?
Jadi di inline conditional rendering ini, kita menggunakan && logical operator dan ternary operator.
Pertama, kita akan membuat yang menggunakan && logical operator.
function App() {
const [pokemonData, getPokemonData] = useState();
useEffect(() => {
getData().then(data => getPokemonData(data));
}, []);
return(
<div>
{pokemonData.types && pokemonData.types.map(pokemon => (
<div key={pokemon.type.name}>{pokemon.type.name}</div>
))}
<div>
)
}
Kenapa cara ini bisa works? Hal ini dijelaskan pada dokumentasi react.
Jadi menurut dokumentasinya, cara ini works karena pada JavaScript true && expression selalu evaluates to expression dan false && expression akan selalu evaluates to false. Jadi jika kondisinya true, maka element setelah && akan dieksekusi. Jika kondisinya false, react akan mengabaikannya dan melewatinya.
Selanjutnya, kita akan membuat contoh yang menggunakan ternary operator yang ketika bernilai false akan menampilkan loading text.
function App() {
const [pokemonData, getPokemonData] = useState();
useEffect(() => {
getData().then(data => getPokemonData(data));
}, []);
return(
<div>
{pokemonData.types ? pokemonData.types.map(pokemon => (
<div key={pokemon.type.name}>{pokemon.type.name}</div>
)) : <div>Loading...</div>}
<div>
)
}
Kita juga dapat menggabungkannya dengan solusi yang pertama yaitu initial value.
function App() {
const [pokemonData, getPokemonData] = useState([]);
useEffect(() => {
getData().then(data => getPokemonData(data));
}, []);
return(
<div>
{pokemonData.types ? pokemonData.types.map(pokemon => (
<div key={pokemon.type.name}>{pokemon.type.name}</div>
)) : <div>Loading...</div>}
<div>
)
}
Perlu diingat ketika kondisinya menjadi kompleks, maka perlu mengganti dari inline conditional rendering mnejadi conditional rendering.
Penutup
Itu tadi cara yang saya temukan berdasarkan hasil pikiran saya dan referensi yang saya dapat di internet dalam menangani TypeError: Cannot read property ‘map’ of undefined, semoga membantu.
Thanks for coming and have a nice day!