Apa Itu Redux? Redux adalah salah satu library state management yang biasa dipakai dengan react.
Fungsinya Apa? Fungsinya sederhana yaitu menyimpan state di satu tempat, sehingga lebih mudah untuk di manage. Biasanya tanpa state management library, kita akan menyimpan state di setiap komponen dan untuk komunikasi bisa dilakukan melalui props.
Berikut gambarannya.
Lah kan ada useContext? Berikut perbedaan redux dengan useContext
useContext | Redux |
useContext is a hook. | Redux is a state management library. |
It is used to share data. | It is used to manage data and state. |
Changes are made with the Context value. | Changes are made with pure functions i.e. reducers. |
We can change the state in it. | The state is read-only. We cannot change them directly. |
It re-renders all components whenever there is any update in the provider’s value prop. | It only re-render the updated components. |
It is better to use with small applications. | It is perfect for larger applications. |
It is easy to understand and requires less code. | It is quite complex to understand. |
Dari perbandingan di atas dapat dilihat dari sisi fungsi sebenarnya tidak harus selalu menggunakan redux karena cukup komplex
Sesuai rekomendasi dari Redux, gunakan Redux jika:
Method yang digunakan untuk mengirim Action yang selajutnya akan diterima oleh Dispatcher lalu diproses oleh Reducers Function yang sesuai.
...
import { useDispatch } from 'react-redux';
...
const dispatch = useDispatch();
...
...
Sebuah JavaScript Object yang mewakili apa yang terjadi di dalam aplikasi.
...
import { useDispatch } from 'react-redux';
...
const dispatch = useDispatch();
const doIncrement = () => {
dispatch(
increment()
)
};
...
...
Function yang menerima object state dari Action yang dikirim, bertugas menentukan bagaimana suatu state diubah.
import { createSlice } from '@reduxjs/toolkit';
const initialState = { value: 0 };
const mycounterSlice = createSlice({
name: 'mycounter',
initialState,
reducers: {
increment(state) {
return {
...state,
value: state.value + 1,
};
},
decrement(state) {
return {
...state,
value: state.value + 1,
};
},
setValue(state, action) {
return {
...state,
value: action.payload,
};
},
},
});
export const { increment, decrement, setValue } = mycounterSlice.actions;
export default mycounterSlice.reducer;
app/store.js
Tempat dimana global state disimpan. Nantinya Reducers akan diimport disini. misal lokasi file untuk Reducers di atas yaitu feature/mycounter/counterSlice.js.
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
import mycounterReducer from '../features/mycounter/counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
mycounter: mycounterReducer,
},
});
Method yang digunakan untuk mendapatkan data dari state yang ada di dalam store.
...
import { useSelector } from 'react-redux';
...
const count = useSelector(state.counter.value);
Menurut primbon dari situs resminya ada baiknya kita mendifinisi redux dari awal membuat projek karena akan langsung menambahkan Redux Toolkit dan React Redux. So kita ikuti. Kita buat project baru bernama my-redux-app.
Jalankan… hasilnya sebagai berikut.
npx create-react-app my-app-redux --template redux
cd my-app-redux
Setelah selesai diinstall hiraukan contoh Counter pada folder features, karena saya juga masih bingung. Kita akan membuat contoh simple aplikasi
Buat Struktur direktori seperti di bawah ini.
src/
Folder ini adalah tempat dimana semua states tinggal dan akan digabungkan ke dalam file index.js. Pada contoh ini kita akan membuat reducer baru bernama todoReducer yang nantinya akan di import ke dalam store.
reducers/todoReducer.js
const initialState = {
todos: [
{
id: 1,
title: "title one",
completed: false
},
{
id: 2,
title: "title two",
completed: false
}
]
}
const todoReducer = (state = initialState, action) => {
const { type, payload} = action;
switch(type){
case "ADD":
return {
...state,
todos: [...state.todos,payload]
}
case "DEL":
return{
...state,
todos: state.todos.filter(todo => todo.id !== payload)
}
default:
return {
...state
}
}
}
export default todoReducer;
Semua reducers digabungkan ke dalam file ini.
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
import todoReducer from '../reducers/todoReducer';
export const store = configureStore({
reducer: {
counter: counterReducer,
todoReducer
},
});
Pada folder ini tempat kita menyimpan semua action atau kegiatan atau juga bisa disebut kejadian. Dan kita akan membuat action yang terhubung dengan data Todo.
actions/todoAction.js
export const addTodo = data => {
return({
type: "ADD",
payload: data
})
}
export const delTodo = data => {
return({
type: "DEL",
payload: data
})
}
store dan action akan disimulasikan disini
components/TodoApp.js
import React from "react";
import { connect } from "react-redux";
import { addTodo, delTodo } from "../actions/todoAction"
const TodoApp= ({ todos, addTodo, delTodo }) => {
const addNewTodo = () => {
const data = {
id:3,
title: "This is three",
complete: false
}
addTodo(data)
}
return(
todo app
{todos.map(todo =>
{todo.title}
)}
)
}
const mapStateToProps = state => ({
todos: state.todoReducer.todos
})
export default connect(mapStateToProps,{addTodo, delTodo})(TodoApp);
Selain menggunakan connect, kita juga bisa menggunakan hooks untuk menghubungkan komponen dan reduxnya.
Ada 2 hooks yang akan digunakan disini yaitu useSelector dan useDispatch.
Kodenya sebagai berikut:
import React from "react";
import {useSelector, useDispatch} from "react-redux";
import {addTodo, delTodo} from "../actions/todoAction"
const TodoApp= () => {
const todos = useSelector(state => state.todoReducer.todos);
const dispatch = useDispatch();
const addNewTodo = () => {
const data = {
id:3,
title: "This is three",
complete: false
}
dispatch(addTodo(data))
}
return(
todo app
{todos.map(todo =>
{todo.title}
)}
)
}
export default TodoApp;
Kedepannya kita akan menggunakan metode Hook.
Provider diletakan di file utama yaitu index.js
import React from 'react';
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import { store } from './app/store';
import TodoApp from './components/TodoApp';
import reportWebVitals from './reportWebVitals';
const container = document.getElementById('root');
const root = createRoot(container);
root.render(
);
reportWebVitals();
Artinya store sudah dipanggil dari awal render aplikasi melalui tag Provider dan kita dapat melihat bahwa file2 di atas berkaitan satu sama lain.
Tentu saja kedepannya dapat dibaca dan dimanipulasi oleh komponen manapun.
Struktur Direktori :
src
– app
– – store.js
– features
– – mycounter
– – – Counter.js App
– – – counterSlice.js Slice
– index.js
import { createSlice } from '@reduxjs/toolkit';
const initialState = { value: 0 };
const counterSlice = createSlice({
name: 'mycounter',
initialState,
reducers: {
increment(state) {
return {
...state,
value: state.value + 1,
};
},
decrement(state) {
return {
...state,
value: state.value + 1,
};
},
setValue(state, action) {
return {
...state,
value: action.payload,
};
},
},
});
export const { increment, decrement, setValue } = counterSlice.actions;
export default counterSlice.reducer;
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { increment, decrement, setValue } from './counterSlice';
import logo from '../../logo.svg';
import '../../App.css';
const Counter = () => {
const counter = useSelector(state => state.mycounter);
const dispatch = useDispatch();
const doIncrement = () => {
dispatch(
increment()
)
};
const doDecrement = () => {
dispatch(
decrement()
)
};
const doReset = () => {
dispatch(
setValue(0)
)
};
return (
Total Saldo
{counter.value}
)
}
export default Counter;
Tambahkan counterSlice ke app/store.js
...
import counterReducer from '../features/counter/counterSlice';
import mycounterReducer from '../features/mycounter/counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
mycounter: mycounterReducer
},
});
Jangan lupa tambahkan Component ke index.js
...
import { store } from './app/store';
import CounterApp from './features/mycounter/Counter';
...
const container = document.getElementById('root');
const root = createRoot(container);
root.render(
);
...
Redux store pada dasarnya tidak ‘mengerti’ apa itu logika asynchronous. Diantara tugas Redux store adalah melakukan proses dispatch action, update state dengan cara memanggil function reducer dan memberi tahu UI bahwa state telah berubah.
Dengan menambahkan sebuah middleware diantara dispatch & reducer, kita bisa intercept action yang sudah di-dispatch sebelum diteruskan ke reducer.
Langsung kita buat saja.
Struktur Direktori :
src
– app
– – store.js
– features
– – posts
– – – Post.js App
– – – postsSlice.js Slice
– index.js
buat file baru features/post/index.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
const initialState = {
data: [],
total :0,
status: 'idle',
error: null
};
export const fetchPosts = createAsyncThunk('posts/fetchPosts', async (limit) => {
const response = await fetch(`https://dummyjson.com/posts?limit=${limit}`);
const data = await response.json();
return data;
});
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {
},
extraReducers: (builder) => {
builder
.addCase(fetchPosts.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchPosts.fulfilled, (state, action) => {
console.log(action.payload)
state.status = 'success';
state.data = action.payload.posts;
state.total = action.payload.total;
})
.addCase(fetchPosts.rejected, (state, action) => {
state.status = 'failed';
state.error = action.error.message;
})
}
});
export const getAllPosts = (state) => state.posts.data;
export const { postAdded } = postsSlice.actions;
export default postsSlice.reducer;
Penjelasan :
Dalam melakukan fetch API dikenal yang namanya proses Asynchronous. Kita bisa memantau progress ini sebagai suatu state.
Value dari state ini berada diantara 4 kondisi:
Kita buat initialState agar bisa menyimpan status dengan inisialisasi awal “idle” sementara state error akan menampung log error saat proses terjadi.
const initialState = {
data: [],
total :0,
status: 'idle',
error: null
};
Isi dari state yang ada di dalam store sekarang adalah:
Buat action function fetchPosts untuk mengambil data dari API.
export const fetchPosts = createAsyncThunk('posts/fetchPosts', async (limit) => {
const response = await fetch(`https://dummyjson.com/posts?limit=${limit}`);
const data = await response.json();
return data;
});
Kemudian tambahkan extraReducer untuk menghandle hasil promise dari fungsi fetchPosts yang berjenis createAsyncThunk di atas.
const postsSlice = createSlice({
...
extraReducers: (builder) => {
builder
.addCase(fetchPosts.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchPosts.fulfilled, (state, action) => {
console.log(action.payload)
state.status = 'success';
state.data = action.payload.posts;
state.total = action.payload.total;
})
.addCase(fetchPosts.rejected, (state, action) => {
state.status = 'failed';
state.error = action.error.message;
})
}
...
...
...
import postsReducer from '../features/post/postsSlice'
export const store = configureStore({
reducer: {
...
posts: postsReducer
},
});
import { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { getAllPosts, fetchPosts } from './postsSlice';
import React from 'react';
const Post =()=>{
const dispatch = useDispatch();
const posts = useSelector(getAllPosts);
const postsStatus = useSelector((state) => state.posts.status);
const total = useSelector((state) => state.posts.total);
const error = useSelector((state) => state.posts.error);
useEffect(() => {
if (postsStatus === 'idle') {
dispatch(fetchPosts(100));
}
}, [postsStatus, dispatch]);
return (
postsStatus === 'loading' ?
:
postsStatus === 'success'?<>
ID
Title
Author
{posts.map((post) => {
return
{post.id}
{post.title}
{post.body}
})}
Total data {total}
>:
{error}
)
}
export default Post
Update index.js
...
import { store } from './app/store';
import Post from './features/post';
...
const container = document.getElementById('root');
const root = createRoot(container);
root.render(
);
...
Untuk tampilan table bisa disesuaikan dengan selera masing-masing, disini saya menggunakan Bulma. Hasilnya akan muncul loading.
Sampai data selesai di fetch.