Memahami Reduce di JavaScript

July 24, 2019
Fariz Rizaldy

Fariz Rizaldy

Engineering-things @ evilfactorylabs

Berfikir fungsional benar-benar mengubah cara pandang gue terhadap sesuatu. Dalam konteks ini adalah tentang bagaimana gue berhadapan dengan data beserta manipulasinya. Ada beberapa built-in method seputar "gaya" fungsional di JavaScript, seperti .map(), .filter(), dan .reduce().

Dan menurut gue, methods yang paling sulit dipahami adalah .reduce(). Gue hampir sebisa mungkin menghindari penggunaan reduce dikeseharian gue. Kalau gue berfikir wah masalah ini bisa diatasi dengan reduce nih, gue akan cari cara lain berdasarkan kasus tersebut dengan bantuan .map() ataupun .filter().

Dan ternyata gue gak bisa terus-terusan menghindari ini.

"Gue harus ngerti reduce", ucap gue.

Dan setelah menghadapi berbagai kasus, mencoba meyakinkan diri untuk sebisa mungkin menggunakan reduce, akhirnya gue mengerti sedikit tentang reduce. Dan akan gue bahas disini berikut dengan studi kasusnya. Akan gue bahas 2 studi kasus dari yang paling gampang sampai ke yang lumayan susah.

Dasar reduce

Array.reduce pada dasarnya adalah salah satu keluarga Higher-order function, yang mana menjadikan banyak nilai menjadi satu nilai.

Oke teori gue ini jelek banget, silahkan googling sendiri artinya. Singkatnya, biar lebih mudah dipahami, lo punya kumpulan nilai berikut: [1, 2, 3, 4, 5]. Pertanyaannya, bagaimana cara mendapatkan "nilai total/sum" dari kumpulan nilai tersebut?

Dengan cara imperatif kita bisa melakukannya dengan iterasi:

  • Cari jumlah perulangan yang harus dilakukan
  • Lakukan perulangan selama jumlah tersebut
  • Didalam perulangannya, tambahkan nilai2 yang ada

Alias seperti ini:

const data = [1, 2, 3, 4, 5]
let result = 0
for (let i = 0; i < data.length; i++) {
result += data[i]
}
window.alert(result) // 15

Silahkan coba dengan mengklik tombol dibawah



Cara diatas is so imperatif, kita masih "harus ngejelasin" apa yang harus dilakuin. Mari bandingkan dengan cara fungsional ini.

const data = [1, 2, 3, 4, 5]
const result = data.reduce((acc, curr) => acc + curr, 0)
window.alert(result) // 15


Hanya. 3. Baris. Tidak ada "penjelasan" bagaimana cara melakukannya. Jadi pertanyaannya Bagaimana caranya agar mendapatkan nilai 15 dari data yang diatas, jawabannya adalah dengan cara menambahkan nilai-nilai yang ada di data aja, kan?

Untuk apa melakukan proses perulangan?

Kayak lu ditanya "gimana caranya agar menjadi sarjana?", terus lu jawabannya adalah harus KKN dulu harus skripsi dulu, dsb berbelit-belit padahal jawaban simplenya adalah dengan wisuda. Ya, untuk bisa wisuda kamu harus KKN, KP, Skripsi dulu tapi lihat dong konteks pertanyaan nya gimana.

Functional adalah tentang paradigma, cara pandang. Untuk apa berfikir kompleks kalau bisa disederhanakan dan memiliki hasil akhir yang sama bro.

Show me some MDN material, riz

Okee gan, pada dasarnya reduce hanya menerima 2 parameter yakni function (you know the HoF-thing) dan initial value. Karena doi HoF, singkatnya adalah function yang kembalian atau gak argumennya adalah function juga.

Oh iya lupa belum disebut kalau reduce ini sebenernya adalah "rekursif", secret sauce dari reduce. Perulangan dan Rekursif hampir sama secara perilaku, sama-sama melakukan suatu proses terus-menerus sampai kepada kondisi tertentu. Bila loop biasanya terus-menerus lakukan sesuatu sampai "counter" mencapai jumlah elemen yang ingin diulang, direkrusif pun sama, tergantung "sesuatu" kondisi nya sampai bagaimana.

Yang mana dalam konteks reduce ini adalah sampai semua elemen yang ada di array tersebut sudah dipanggil callback nya alias si reducer.

Oke balik lagi, argumen pertama dari reduce adalah function, kan? Function tersebut adalah callback yang akan dieksekusi dari setiap nilai dari array yang ada. Callback tersebut menerima 4 argumen:

  • accumulator
  • currentValue
  • index, optional
  • array, optional

Detail diatas berdasarkan MDN, ya. Gue biasanya hanya menggunakan 2 parameter yang wajib aja, yakni accumulator (yang biasa ditulis acc) dan currentValue (yang biasa ditulis curr). Nilai dari accumulator didapat dari hasil callback (reducer) sebelumnya Kecuali ketika pertama kali invokasi, akan mengambil nilai dari hasil callback terhadap nilai dari initialValue.

Lalu currentValue adalah elemen "aktif" yang sedang di proses (alias the array[i]).

Meskipun initialValue bersifat opsional, namun direkomendasikan untuk mengisinya untuk menghindari TypeError. Dan ya, bila nilai initialValue tidak diberikan, maka proses pemanggilan callback akan langsung dimulai dari elemen array pertama.

Studi Kasus

Gue akan mengambil kasus nyata dari penggunaan array yang lumayan sedikit kompleks. Kita ambil dari contoh Bukalapak (not affiliated & not working there). Kebetulan gue dapet kasus yang hampir mirip dengan kasus ini.

Silahkan buka gambar di tab baru untuk gambar yang lebih jelas.

Yang mana di kasus gue adalah "mengelompokkan" data Cart berdasarkan penjual dengan kasus, struktur data yang disimpan (both in local db & db) adalah seperti ini:

[
{
"_id":"5d3774e542211ab79529a9eb",
"name":"[redacted]",
"image":"d31d64a686dff599f80198b638ae0fdd",
"contextId":"5d2a45d84780932b236200c2",
"price":150000000,
"stocks":2
},
{
"_id":"5d3774e542211ab79529a9ea",
"name":"[redacted]",
"image":"d31d64a686dff599f80198b638ae0fdd",
"contextId":"5d2a45d84780932b236200c2",
"price":12500000,
"stocks":1
},
{
"_id":"5d3774e542211ab79529a9ea",
"name":"[redacted]",
"image":"d31d64a686dff599f80198b638ae0fdd",
"contextId":"5d2a45d84766632b236200c9",
"price":12500000,
"stocks":9
}
]

Kunci nya adalah di contextId. Anggap Context merepresentasikan "pelapak". Gue ingin mengelompokkan data-data tersebut berdasarkan contextId sehingga misalnya bisa menampilkan data seperti diatas (yang Bukalapak).

Mungkin kita bisa menggunakan cara klasik: loop, assign via newGroupedArray[contextId] = { someObject }, dst. Dan di kasus gue:

  • Nilai subtotal bergantung dengan total dari "grouped" context (grouped.price + grouped + price)
  • Array object diatas sifatnya Observable
  • Jika stocks berubah, request ke db (+ write ke ls) dengan debounce per 3 detik untuk kebutuhan beacon dan persist data

Melihat kasus diatas entah mengapa gue enggak tertarik menggunakan cara looping.

Action

Tujuan sudah jelas, untuk "mengelompokkan" secara temporary, yang berada di computed object (haloo vue). Kuncinya adalah contextId, mari kita menulis kodenya:

someArray.reduce((acc, curr) => {
const key = curr['contextId']
if (!acc[key]) {
acc[key] = []
}
acc[key].push(curr)
return acc
}, {})

Gue memilih mengelompokkan kedalam object dengan key contextId because why not.

Penjelasan singkatnya:

  • Apakah "nilai" dari contextId sudah ada? Jika belum, buat dulu dong
  • Lalu tambahkan nilai array sesuai dengan nilai dari contextId

Hasil:

{
"5d2a45d84780932b236200c2": [
{
"_id":"5d3774e542211ab79529a9eb",
"name":"[redacted]",
"image":"d31d64a686dff599f80198b638ae0fdd",
"contextId":"5d2a45d84780932b236200c2",
"price":150000000,
"stocks":2
},
{
"_id":"5d3774e542211ab79529a9ea",
"name":"[redacted]",
"image":"d31d64a686dff599f80198b638ae0fdd",
"contextId":"5d2a45d84780932b236200c2",
"price":12500000,
"stocks":1
},
],
"5d2a45d84766632b236200c9": [
{
"_id":"5d237822bac80a215315a9a8",
"name":"[redacted]",
"image":"d31d64a686dff599f80198b638ae0fdd",
"contextId":"5d2a45d84766632b236200c9",
"price":12500000,
"stocks":9
}
]
}

Implementasi di UI:

<div
v-for="(context, contextId) in groupedSomething"
:key="contextId"
>
<h4>{{ contextId }}</h4>
<div
v-for="something in context"
:key="something._id"
>
{{ something.name }}
</div>
</div>

Dan hasilnya adalah apa yang diharapkan! Dan silahkan tambahkan beberapa meta field daripada hanya bergantung kepada contextId sebagai identifier di UI. Misal diubah menjadi:

{
"5d2a45d84766632b236200c9": {
"label": "Some helpful information",
"data": [{...}]
}
}

Eh iya di kode diatas ada statement "if" dan biasanya anak functional gak kenal dengan if. Mari kita ubah menjadi gaya so fungsional plus tidak mendeklarasi variable satupun:

someArray.reduce((acc, curr) => {
(acc[curr['contextId']] = acc[curr['contextId']] || []).push(curr)
return acc
}, {})

Yang mana cara penjelasannya sama saja seperti sebelumnya, bedanya hanya gaya penulisannya saja. Zero if, terkadang cinta tak perlu alasan dan pengorbanan bro sis.

Kesimpulan

Semoga penjelasan singkat tersebut mudah dipahami. Mental model dasarnya adalah ada di proses yang dilakukan secara terus menerus.

Jika looping adalah proses yang dilakukan secara terus menerus yang diatur dari nilai pada suatu variable, rekursif diatur oleh suatu "statement" kondisi.

Di kasus reduce ini adalah sampai callback/reducer terpanggil di semua currentValue.

Kasus Lain

  • Flatten array
  • Menghitung jumlah nilai unique dari suatu array
  • BLOCKCHAIN BRO
  • Use your imagination