Table of Contents

Pengenalan
Apa itu Redux

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.

Image from codecentric

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.
Haruskah menggunakan redux?

Dari perbandingan di atas dapat dilihat dari sisi fungsi sebenarnya tidak harus selalu menggunakan redux karena cukup komplex

Kapan harus menggunakan redux?

Sesuai rekomendasi dari Redux, gunakan Redux jika:

  1. Banyak data yang berubah dari waktu ke waktu
  2. Pengelolaan state harus dilakukan di satu tempat (Single source of truth)
  3. Mengelola state di top-level component sudah tidak lagi relevan
Konsep Dasar
Flow
  1. Pengguna berinteraksi dengan View untuk memicu pembaruan state.
  2. Saat pembaruan state diperlukan, View mengirim Action melalui perintah dispatch.
  3. Reducers menerima Action dari dispatch dan mengubah state di dalam Store sesuai ketentuan Action yang dikirim.
  4. View menerima perubahan state dari Store.
Dispatch

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();
    ...
...
				
			
Action

Sebuah JavaScript Object yang mewakili apa yang terjadi di dalam aplikasi.

				
					...
import { useDispatch } from 'react-redux';
...
    const dispatch = useDispatch();
    const doIncrement = () => {
        dispatch(
            increment()
        )
    };
    ...
...
				
			
Reducers

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;
				
			
Store

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,
  },
});

				
			
Selector

Method yang digunakan untuk mendapatkan data dari state yang ada di dalam store.

				
					...
import { useSelector } from 'react-redux';
...
    const count = useSelector(state.counter.value);
				
			
Memulai Project

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.

				
					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 

  1. ToDoApp(tanpa menggunakan Redux Toolkit)
  2. MyCounter(menggunakan Redux Tookit).
Membuat TodoApp

Buat Struktur direktori seperti di bawah ini.

src/

  • actions
    • todoAction.js
  • app
    • store.js
  • components
    • TodoApp.js
  • reducers
    • todoReducer.js
  • index.js
Reducer

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;
				
			
Menghubungkan Reducer ke dalam Store

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
  },
});

				
			
Actions

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
})
}
				
			
Komponen Todo

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(
        <div>
            <h1>todo app</h1>
            <button onClick={addNewTodo}>add</button>
            {todos.map(todo =>
                <div key={todo.id}>
                    <p>{todo.title}</p>
                    <button onClick={() => delTodo(todo.id)}>delete</button>
                </div>
            )}
        </div>
    )
}
const mapStateToProps = state => ({
  todos: state.todoReducer.todos
})
export default connect(mapStateToProps,{addTodo, delTodo})(TodoApp);
				
			
Hook

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(
    <div>
      <h1>todo app</h1>
      <button onClick={addNewTodo}>add</button>
      {todos.map(todo =>
        <div key={todo.id}>
          <p>{todo.title}</p>
          <button onClick={() => dispatch(delTodo(todo.id))}>
            delete
          </button>
        </div>
      )}
    </div>
  )
}
export default TodoApp;
				
			

Kedepannya kita akan menggunakan metode Hook.

Provider

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(
  <React.StrictMode>
    <Provider store={store}>
      <TodoApp/>
    </Provider>
  </React.StrictMode>
);
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.

Membuat App Counter dengan Redux Tookit

Struktur Direktori :

src
 – app
 – – store.js
 – features
 – – mycounter
 – – – Counter.js App
 – – – counterSlice.js Slice
 – index.js

Slice
				
					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;
				
			
App
				
					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 (
    <div className="App">
        <img src={logo} className="App-logo" alt="logo" />
        <section>
            <h2>Total Saldo</h2>
            <p>{counter.value}</p>
        </section>
        <section>
            <button type="button" onClick={doIncrement}>+</button>
            <button type="button" onClick={doDecrement}>-</button>
            <button type="button" onClick={doReset}>Reset</button>
        </section>
    </div>
  )
}
export default Counter;
				
			
Update Store

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
  },
});
				
			
Panggil

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(
  <React.StrictMode>
    <Provider store={store}>
      <CounterApp/>
    </Provider>
  </React.StrictMode>
);
...
				
			
Membuat Post(Fetch API) dengan Middleware

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

Slice
				
					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:

  1. Request belum dikirim (idle)
  2. Request masih dalam proses
  3. Request berhasil
  4. Request gagal

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:

  1. data, menyimpan semua data posts,
  2. total, untuk menampung nilai total dari posts
  3. status, menyimpan status dari proses async
  4. error, menyimpan error jika ada error

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;
      })
  }
  ...
...
				
			
Update Store
				
					...
import postsReducer from '../features/post/postsSlice'

export const store = configureStore({
  reducer: {
    ...
    posts: postsReducer
  },
});
				
			
App
				
					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' ? 
            <progress className="progress is-small is-primary" max="100">15%</progress>:
        postsStatus === 'success'?<>
        <div className="table-container">
        <table className="table table is-striped" >
            <thead>
                <tr className="has-background-primary">
                    <th className="has-text-light">ID</th>
                    <th className="has-text-light"><abbr title="Title">Title</abbr></th>
                    <th className="has-text-light">Author</th>
                </tr>
            </thead>
            <tbody>
                {posts.map((post) => {
                    return <tr key={post.id}>
                        <td>{post.id}</td>
                        <th>{post.title}</th>
                        <td>{post.body}</td>
                    </tr>
                })}
                
            </tbody>
        </table>
        </div>
        <p className="p-5">Total data {total}</p></>:
        <div>{error}</div>
    )
}
export default Post
				
			
Panggil

Update index.js

				
					...
import { store } from './app/store';
import Post from './features/post';
...
const container = document.getElementById('root');
const root = createRoot(container);

root.render(
  <React.StrictMode>
    <Provider store={store}>
      <Post/>
    </Provider>
  </React.StrictMode>
);
...
				
			

Untuk tampilan table bisa disesuaikan dengan selera masing-masing, disini saya menggunakan Bulma. Hasilnya akan muncul loading.

Sampai data selesai di fetch.