Dalam ekosistem pengembangan React, manajemen state seringkali menjadi topik diskusi yang panjang. Bertahun-tahun, Redux menjadi solusi de facto untuk aplikasi berskala besar, dikenal karena prediktabilitas dan developer tools-nya yang canggih. Namun, seiring dengan evolusi React Hooks, banyak developer mulai mempertanyakan relevansi dan kompleksitas Redux untuk setiap project. Saya pribadi sering melihat developer terjebak dalam boilerplate Redux yang masif, padahal kebutuhan state manajemen mereka bisa diselesaikan dengan jauh lebih sederhana menggunakan fitur bawaan React.
Artikel ini akan membawa Anda menyelami berbagai cara mengelola state di aplikasi React Anda tanpa perlu menyentuh Redux. Kita akan membahas solusi bawaan React seperti useState, useReducer, useContext, hingga custom hooks, serta kapan dan bagaimana menggunakannya secara efektif. Tujuannya adalah membantu Anda membangun aplikasi React yang lebih rapi, efisien, dan mudah dirawat, dengan pemahaman mendalam tentang praktik terbaik manajemen state.
Mengapa Banyak Developer Mulai Beralih dari Redux?
Sebelum kita menyelami alternatifnya, mari kita pahami mengapa banyak developer mulai mencari cara lain. Redux, dengan arsitektur Flux-nya, menawarkan manajemen state yang sangat terstruktur dan prediktif. Ini bagus untuk aplikasi yang sangat kompleks dengan state global yang berubah sering dan perlu dilacak dengan ketat. Namun, untuk banyak aplikasi, terutama yang skalanya menengah ke bawah, kurva pembelajaran dan boilerplate code yang dihasilkan Redux terasa berlebihan.
Setiap perubahan state di Redux biasanya melibatkan action, reducer, dan store. Ini berarti untuk fungsionalitas sederhana, Anda mungkin perlu membuat tiga file baru dan menulis beberapa baris kode yang terpisah. Bagi sebagian developer, ini terasa seperti menggunakan “palu godam untuk memaku paku kecil”. Kehadiran React Hooks yang memungkinkan stateful logic dan side effects langsung di komponen fungsional, semakin memperkuat argumen untuk solusi yang lebih ringan dan terintegrasi langsung dengan React.
State Lokal Komponen: useState
useState adalah hook paling dasar dan sering digunakan untuk mengelola state dalam komponen fungsional. Ini adalah pengganti langsung untuk this.state dan this.setState pada komponen berbasis kelas. Jika Anda hanya perlu menyimpan dan memperbarui data yang hanya relevan untuk satu komponen dan turunannya secara langsung, useState adalah pilihan terbaik dan termudah.
Cara Penggunaan useState
Anda bisa mendeklarasikan sebuah state dan fungsi untuk memperbaruinya dengan memanggil useState. Misalnya, untuk mengelola jumlah hitungan, Anda akan menulis sesuatu seperti const [count, setCount] = useState(0); di dalam komponen fungsional Anda. count akan menyimpan nilai state saat ini, dan setCount adalah fungsi yang bisa Anda panggil untuk memperbarui nilai tersebut. Nilai 0 adalah nilai awal state.
Kapan Menggunakan useState?
- Untuk state yang sederhana dan terisolasi dalam satu komponen.
- Ketika state tidak perlu dibagikan ke banyak komponen yang berbeda level hirarki.
- Contoh: state untuk input form, visibilitas modal, toggle UI (seperti dark/light mode lokal), atau status loading internal.
Keterbatasan useState
Ketika aplikasi mulai membesar dan state harus dibagikan ke banyak komponen yang tidak saling berdekatan, Anda mungkin akan mengalami “prop drilling” (mengirimkan props dari induk ke anak, ke cucu, dan seterusnya). Ini membuat kode sulit dibaca dan dirawat. Untuk kasus ini, kita perlu solusi yang lebih global.
State Lebih Kompleks: useReducer
useReducer adalah alternatif dari useState untuk mengelola state yang lebih kompleks, terutama ketika logika pembaruan state melibatkan banyak sub-nilai atau ketika state selanjutnya bergantung pada state sebelumnya. Ini mirip dengan Redux dalam konsepnya, menggunakan fungsi reducer untuk menentukan bagaimana state berubah berdasarkan action yang dikirimkan.
Cara Penggunaan useReducer
useReducer menerima dua argumen: fungsi reducer dan nilai state awal. Ini mengembalikan state saat ini dan fungsi dispatch. Contohnya: const [state, dispatch] = useReducer(reducer, initialState);. Fungsi reducer menerima state saat ini dan sebuah action, lalu mengembalikan state yang baru. Action biasanya objek dengan properti type dan payload.
Kapan Menggunakan useReducer?
- Untuk state yang memiliki logika pembaruan yang kompleks (misalnya, lebih dari sekadar mengganti nilai).
- Ketika state melibatkan banyak bagian yang saling terkait dan berubah bersamaan.
- Ketika Anda ingin mengelola state yang mirip dengan state machine.
- Contoh: manajemen keranjang belanja, form dengan validasi multi-field, atau state untuk fitur drag-and-drop.
Pengalaman Praktis dengan useReducer
Saya pribadi sering beralih ke useReducer ketika useState mulai terasa kewalahan. Misalnya, pada sebuah form dengan beberapa input dan validasi yang saling bergantung, menggunakan useState untuk setiap input akan menghasilkan banyak sekali fungsi setState. Dengan useReducer, saya bisa mengirimkan satu action seperti { type: ‘UPDATE_FIELD’, field: ‘email’, value: ‘…’ }, dan reducer akan menangani logika pembaruan state serta validasinya secara terpusat. Ini membuat logika lebih mudah dipahami dan diuji. Namun, tetap ingat bahwa useReducer ini masih bersifat lokal untuk komponen tersebut atau komponen yang menerima state dan dispatch melalui props.
State Global: useContext
Ketika Anda perlu membagikan state ke banyak komponen di berbagai level hirarki tanpa “prop drilling”, useContext adalah jawabannya. Ini adalah mekanisme React untuk memungkinkan data “mengalir” ke bawah di pohon komponen tanpa harus melewati setiap level secara manual. Anda dapat menggabungkan useContext dengan useState atau useReducer untuk menciptakan state global yang dapat diakses dan diperbarui dari mana saja.
Cara Penggunaan useContext
Langkah-langkahnya meliputi:
- Membuat Context: Gunakan React.createContext() untuk membuat sebuah objek context.
- Menyediakan Context: Bungkus komponen induk Anda (biasanya di level aplikasi teratas) dengan Context.Provider dan berikan nilai state yang ingin dibagikan melalui prop value.
- Mengonsumsi Context: Di komponen anak mana pun, gunakan hook
useContext(MyContext)untuk mengakses nilai state yang disediakan.
Kapan Menggunakan useContext?
- Untuk state yang benar-benar global dan dibutuhkan oleh banyak komponen di berbagai level.
- Ketika Anda ingin menghindari “prop drilling” yang berlebihan.
- Contoh: tema aplikasi (dark/light mode), informasi autentikasi pengguna, atau pengaturan bahasa.
Kombinasi useContext dan useReducer untuk State Global yang Kuat
Ini adalah pola yang sangat kuat dan sering saya gunakan sebagai alternatif Redux. Anda bisa menempatkan state yang dikelola oleh useReducer di dalam sebuah provider, dan fungsi dispatch-nya juga bisa dibagikan melalui context. Dengan demikian, komponen mana pun di bawah provider dapat membaca state dan mengirimkan action untuk memperbaruinya, mirip dengan cara kerja Redux, tetapi dengan lebih sedikit boilerplate dan lebih terintegrasi dengan React.
Dalam praktiknya, Anda akan membuat sebuah komponen Provider khusus yang menampung logika useReducer. Kemudian, Anda akan membuat dua context: satu untuk state, satu lagi untuk fungsi dispatch. Ini memungkinkan komponen mengonsumsi hanya apa yang mereka butuhkan (state atau dispatch), mengoptimalkan performa dengan mencegah re-render yang tidak perlu.
Mengatasi “Prop Drilling” dengan Lebih Elegansi
“Prop drilling” adalah masalah umum yang terjadi ketika Anda harus meneruskan data dari komponen induk ke komponen anak yang jauh di bawah pohon komponen, bahkan jika komponen di antaranya tidak memerlukan data tersebut. Ini membuat kode berantakan dan sulit dikelola.
Cara Mengatasi Prop Drilling:
- useContext: Seperti yang sudah dibahas, ini adalah solusi paling langsung untuk menghindari prop drilling.
- Component Composition: Terkadang, Anda bisa mengubah struktur komponen Anda. Daripada meneruskan data melalui props, Anda bisa meneruskan komponen anak sebagai prop children. Ini memungkinkan komponen induk untuk memiliki kontrol atas tata letak, sementara komponen anak bertanggung jawab atas datanya sendiri.
- Custom Hooks: Untuk logika stateful yang kompleks dan sering digunakan kembali, membuat custom hook bisa sangat membantu. Ini memungkinkan Anda mengekstrak logika state dan membagikannya ke beberapa komponen, mirip dengan cara kerja
useContext, tetapi dengan fokus pada logika yang dapat digunakan kembali.
Custom Hooks: Mengenkapsulasi Logika Stateful
Custom Hooks adalah cara ampuh untuk menggunakan kembali logika stateful di antara komponen. Jika Anda memiliki logika state yang kompleks atau efek samping yang sama di beberapa komponen, Anda bisa mengekstraknya ke dalam sebuah custom hook. Ini bukan tentang manajemen state global, melainkan tentang reuseability dan memisahkan perhatian.
Cara Membuat dan Menggunakan Custom Hook
Sebuah custom hook hanyalah sebuah fungsi JavaScript yang namanya diawali dengan “use” (misalnya, useLocalStorage, useFormInput, useFetchData) dan bisa memanggil hook React lainnya (seperti useState atau useEffect). Misalnya, Anda bisa membuat useToggle untuk mengelola state boolean yang sering dipakai untuk membuka/menutup sesuatu.
Kapan Menggunakan Custom Hooks?
- Ketika Anda perlu menggunakan kembali logika stateful di beberapa komponen.
- Untuk membersihkan komponen Anda dari logika yang kompleks.
- Contoh: hook untuk mengambil data dari API, mengelola input form, menggunakan local storage, atau untuk timer.
Pengalaman dengan Custom Hooks
Saya sering menggunakan custom hooks untuk hal-hal yang berulang. Daripada menulis useState dan useEffect untuk fetch data di setiap komponen, saya akan membuat useFetch(url). Ini tidak hanya membuat komponen lebih bersih tetapi juga membuat logika pengambilan data terpusat dan mudah diubah jika ada perubahan persyaratan. Ini juga sangat membantu untuk memisahkan concerns.
Memilih Solusi State Management yang Tepat
Tidak ada satu solusi “terbaik” untuk semua kasus. Pilihan Anda harus didasarkan pada kompleksitas aplikasi, ukuran tim, dan kebutuhan spesifik proyek.
- Aplikasi Kecil-Menengah: Mulailah dengan
useStatedan beralih keuseReducerjika state menjadi terlalu kompleks dalam satu komponen. - Kebutuhan State Global: Jika ada state yang benar-benar perlu dibagikan ke banyak komponen, pertimbangkan kombinasi
useContext+useReducer. Ini memberikan kekuatan Redux tanpa beban. - Logika yang Bisa Digunakan Kembali: Ekstraksi logika stateful ke custom hooks akan membuat kode lebih modular dan mudah dirawat.
Pertimbangan Performa
Saat menggunakan useContext, perlu diingat bahwa perubahan nilai di provider akan menyebabkan semua komponen yang mengonsumsi context tersebut untuk di-render ulang. Untuk menghindari re-render yang tidak perlu, Anda bisa:
- Memisahkan context menjadi beberapa bagian (misalnya, satu context untuk state dan satu lagi untuk fungsi dispatch).
- Menggunakan
React.memopada komponen anak yang tidak perlu di-render ulang jika props-nya tidak berubah. - Menggunakan
useMemodanuseCallbackuntuk memmemo nilai dan fungsi yang diberikan ke context provider.
Masalah yang Sering Terjadi dan Solusinya
Saat mengelola state tanpa Redux, ada beberapa jebakan umum yang sering dihadapi developer:
1. Terlalu Banyak Re-render pada useContext
Gejala: Komponen-komponen Anda sering di-render ulang meskipun data yang mereka tampilkan tidak berubah. Ini sering terjadi ketika seluruh objek state dibagikan melalui satu useContext provider, dan setiap perubahan kecil pada objek tersebut akan memicu re-render di semua konsumen.
Penyebab: Konsumen context di-render ulang setiap kali nilai prop value dari Context.Provider berubah. Jika Anda melewatkan objek atau array langsung sebagai value tanpa memoization, objek tersebut akan dianggap “berbeda” di setiap render, bahkan jika isinya sama.
Solusi:
- Pisahkan context menjadi beberapa. Misalnya, satu context untuk state itu sendiri dan satu lagi untuk fungsi dispatch. Ini memungkinkan komponen hanya mengonsumsi bagian yang mereka butuhkan.
- Gunakan
useMemountuk memoize nilai yang diberikan ke Context.Provider, memastikan nilai yang sama tidak memicu re-render yang tidak perlu jika isinya tidak berubah.
2. Prop Drilling yang Tidak Terkendali
Gejala: Anda harus meneruskan props melalui 3-4 level komponen yang berbeda, meskipun komponen di tengah tidak menggunakan props tersebut. Kode menjadi panjang dan sulit diikuti.
Penyebab: State yang relevan untuk komponen di bagian bawah pohon komponen awalnya dideklarasikan di komponen induk yang sangat tinggi.
Solusi:
- Jika state bersifat global atau dibutuhkan oleh banyak komponen yang jauh, gunakan
useContextuntuk menyediakannya. - Pertimbangkan untuk merestrukturisasi komponen Anda menggunakan composition (meneruskan komponen sebagai prop children) untuk membuat data lebih dekat ke tempat ia dibutuhkan.
3. Logika State Terlalu Tersebar
Gejala: Logika pembaruan state tersebar di berbagai useState atau useEffect di komponen yang berbeda, membuat sulit untuk melacak bagaimana state berubah secara keseluruhan.
Penyebab: State dipecah menjadi unit-unit terlalu kecil atau logika efek samping tidak dikelola dengan baik.
Solusi:
- Gunakan
useReduceruntuk mengonsolidasi logika pembaruan state yang kompleks dalam satu fungsi reducer yang terpusat. - Ekstraksi logika stateful yang dapat digunakan kembali ke dalam custom hooks. Ini membantu memisahkan concern dan membuat kode lebih modular.
4. State Tidak Sinkron Antara Komponen
Gejala: Dua atau lebih komponen seharusnya menampilkan data yang sama, tetapi salah satunya menunjukkan data lama atau tidak terbarui.
Penyebab: Masing-masing komponen memiliki state lokalnya sendiri yang seharusnya menjadi bagian dari state global, atau ada duplikasi logika pembaruan state.
Solusi:
- Angkat state ke komponen induk terdekat yang membutuhkan state tersebut (lifting state up).
- Jika state dibutuhkan di banyak tempat yang tidak saling berdekatan, gunakan
useContextuntuk membuat state tersebut global dan terpusat.
Pengalaman dan Pertimbangan Praktis
Sebagai seorang software engineer yang sehari-hari berkutat dengan React, saya sering melihat bagaimana developer, terutama yang baru, terlalu cepat memutuskan untuk menggunakan Redux hanya karena proyeknya “sudah mulai besar”. Padahal, seringkali kebutuhan mereka bisa terpenuhi dengan baik oleh kombinasi useContext dan useReducer.
Dalam project skala kecil hingga menengah, penggunaan Redux bisa menjadi beban yang tidak perlu. Waktu yang dihabiskan untuk setup, menulis actions, reducers, dan selectors bisa dialokasikan untuk pengembangan fitur. Dengan hooks bawaan React, saya bisa membangun fungsionalitas dengan cepat dan menjaga kode tetap bersih.
Salah satu skenario yang sangat saya sukai adalah membangun “micro-store” menggunakan useContext dan useReducer. Ini memungkinkan saya untuk memiliki beberapa “slice” state global yang berbeda, masing-masing dengan provider dan reducer-nya sendiri. Misalnya, ada satu context untuk autentikasi, satu untuk tema, dan satu lagi untuk data spesifik fitur. Ini memberikan modularitas yang sangat mirip dengan beberapa pendekatan modern Redux Toolkit, tetapi dengan API yang lebih ringan dan terintegrasi langsung dengan ekosistem React.
Pertimbangkan juga biaya sumber daya. Meskipun Redux dikenal efisien, setiap library eksternal membawa beban tambahan. Menggunakan solusi bawaan React berarti Anda mengurangi bundle size aplikasi dan ketergantungan pada pihak ketiga. Ini penting untuk aplikasi yang mementingkan performa dan kecepatan loading.
FAQ
Apakah saya harus menghindari Redux sepenuhnya?
Tidak harus. Redux masih merupakan pilihan yang sangat solid untuk aplikasi berskala enterprise yang sangat besar, tim yang membutuhkan pola yang sangat ketat, atau ketika Anda sudah sangat akrab dengannya. Namun, untuk banyak proyek, alternatif bawaan React sudah lebih dari cukup dan seringkali lebih efisien.
Kapan sebaiknya saya mempertimbangkan state management library lain seperti Zustand atau Jotai?
Setelah Anda menguasai hooks bawaan React (useState, useReducer, useContext) dan merasa bahwa masih ada kebutuhan yang belum terpenuhi (misalnya, performa sangat kritis, atau Anda ingin sintaks yang lebih ringkas dari useContext), barulah pertimbangkan library pihak ketiga yang lebih modern dan ringan seperti Zustand, Jotai, atau Recoil. Mereka menawarkan pengalaman yang lebih sederhana dibanding Redux namun dengan performa dan fitur yang powerful.
Bagaimana cara memastikan performa aplikasi saya tetap baik saat menggunakan useContext?
Pastikan Anda melakukan memoization dengan useMemo untuk nilai yang diberikan ke Context.Provider, dan gunakan React.memo atau useCallback jika diperlukan pada komponen anak. Pisahkan context menjadi beberapa bagian jika memungkinkan, agar komponen hanya di-render ulang jika bagian state yang relevan dengan mereka berubah.
Apakah mungkin mencampur Redux dengan hooks bawaan React?
Sangat mungkin. Banyak aplikasi yang awalnya menggunakan Redux kini secara bertahap mengadopsi hooks untuk state lokal atau bahkan mengganti beberapa bagian state global dengan kombinasi useContext dan useReducer. Anda bisa memiliki keduanya berdampingan selama Anda mengelola dependencies dan arsitektur dengan baik.
Kesimpulan
Mengelola state React tanpa Redux bukan lagi sekadar tren, melainkan sebuah praktik yang semakin matang dan powerful. Dengan useState untuk state lokal, useReducer untuk logika state yang kompleks, dan useContext untuk distribusi state global, Anda memiliki seperangkat alat yang komprehensif untuk membangun aplikasi React yang efisien, mudah di-maintain, dan scalable.
Kunci utamanya adalah memahami kapan harus menggunakan alat yang mana. Jangan buru-buru mengadopsi solusi yang paling kompleks jika kebutuhan Anda masih sederhana. Mulailah dengan yang paling dasar dan tingkatkan kompleksitas sesuai kebutuhan. Dengan pendekatan ini, Anda tidak hanya akan menghemat waktu pengembangan, tetapi juga menghasilkan kode yang lebih bersih dan performa aplikasi yang lebih optimal. Selamat mencoba!
TAGS: React, State Management, React Hooks, useState, useReducer, useContext, Front-end Development, Web Development, Developer Tools



