Apa Itu Redux ?

Redux merupakan back-end yang berkolaborasi dengan react nativeBelajar framework baru merupakan hal yang menantang dan mmanaenyusahkan, namun sekali menemukan alur pengkodean (menulis code) akan lebih mudah untuk mengimplementasikan.

Menjadi seorang pemula dalam Redux membuat saya membaca banyak referensi hal dasar yang harus ada dalam aplikasi berbasis Redux. Setelah membaca lebih dari ratusan kata (lebay, dalam artian lain beberapa artikel), saya menemukan basis dari aplikasi Redux. Basis dalam hal ini yaitu bagian dasar yang bisa membuat aplikasi Redux dapat berjalan.

Implementasi Redux bisa digambarin pake diagram berikut:

Jadi ada 4 aktor utama di dalam sistem Redux:

  1. Action
  2. Store
  3. Reducer
  4. State

App/UI membuat objek Action dan mengirimkannya ke Store. Oleh Store, objek Action diteruskan ke Reducer yang bertugas melakukan update terhadap State. Kalo ada pembaruan State maka Store mengirim objek State yang baru ke semua bagian App yang jadi subscriber / listener.

Action

Action adalah objek sederhana yang wajib punya properti bernama type & bertipe string. Action boleh berisi data lain yang mungkin diperlukan untuk update state, tapi yang paling pokok adalah type.

const add = {   type:'ADD',   value: 100 } const start = {   type: 'START' }

Reducer

Reducer adalah sebuah function yang bertugas memproses Action dan bikin State baru. Reducer punya dua parameter state & action.

const reducer = ( state = {}, action) =>{   if(action.type === ....) {     // blah blah     return newstate;   } else if(action.type === ....) {     // blah blah blah     return otherstate   }   return state; };

Syarat Reducer adalah dia harus berupa pure function. Apa itu ?

Pure Function (PF) adalah function yang:

  1. Selalu memberi nilai balik yang sama selama argumennya sama.
  2. Nggak mengubah (mutate) objek atau variabel lain
  3. Nggak tergantung/terpengaruh objek atau variabel lain

Contoh yang bukan PF:

let ammo = 100; const shoot = () =>{   //ngerubah variabel di luar fn   return ammo--; } const canShoot = ( round ) =>{   //tergantung variabel di luar fn   if( round >= ammo){     return true;   }   return false }

Store

Store adalah objek yang menghubungkan Action & Reducer. Pada intinya, objek ini bertugas:

  1. Menyimpan State
  2. Menyediakan API untuk mengakses State
  3. Menyediakan API untuk update State
  4. Menyediakan API biar objek lain bisa jadi listener / subscriber
  5. Menyediakan API untuk melepas listener / subscriber.

Satu aplikasi hanya boleh punya satu Store. Tapi satu Store bisa punya banyak Reducer. Jadi kalo kita ingin memecah kode yang bertugas menghandel data ke dalam beberapa modul, kita bisa bikin beberapa Reducer & nanti digabung dalam satu Store. Misalnya kayak di bawah ini:

Contoh Aplikasi: Video Player

Redux didistribusikan sebagai modul NPM. Jadi kita instal dulu:

$ yarn init -y && yarn add redux -E -S

Selain Redux, kita pake Poi untuk transpile ES6 & jalanin server lokal.

Template HTML

<!-- file: index.ejs --> <h1>My Video Player</h1> <div id="app">   <video id="myvideo"></video>   <div id="controlbar">     <div>       <input type="text" name="video-source" value="" placeholder="video url">       <button id="load-src-btn">Load</button>     </div>     <div>       <button id="play-pause-btn">Play</button>       <button id="vol-up-btn">Vol+</button>       <button id="vol-down-btn">Vol-</button>       <div id="time">         <span>Time: </span>         <input type="text" name="time" value="0">         <span>/</span>         <span id="duration">00</span>       </div>     </div>   </div> </div>

Stylesheet

Sebenernya nggak penting sih. Tapi ya biar app-nya nggak jelek-jelek amat.

*{   margin:0;padding:0; } body{   font-size: 1.2em;   line-height: 1.5em;   padding:10px; } h1{   margin: 1em 0; } video{   width:640px;   height:360px;   position: relative;   background: #66CCFF; } #controlbar{   line-height: 2em;   font-size:14px;   input[type=text]{     width:90%;     max-width:640px;     padding: 5px;     font-size:100%;   }   input[name=time]{     width:4em;   }   button{     font-size:0.8em;     padding:5px;   }   :last-child{     margin-top:10px;   }   #time{     display:inline-block;     text-align: center;   } }

Terus bikin file entry buat appnya sendiri:

//file: src/index.js import './styles.scss'

Nyalain Poi:

$ poi src/index.js

Terus buka localhost:4000.

Lanjut ke “kodingannya” ( bahasa programmer jaman NOW ).

State, Reducer & Store Minimalistik

Pertama kita bikin object State-nya. Sederhana aja,

//file: src/index.js import './styles.scss' const defaultVideoState = {   source: undefined,   status: undefined,   volume: 1,   duration: 0,   time: 0 };

Lanjut bikin Reducer, masih di file src/index.js.

const videoStateReducer = (state = defaultVideoState, action) => {   return state; };

Terus bikin Store & subscriber:

//import dulu import { createStore } from 'redux'; const videoStore = createStore( videoStateReducer ); //ekspos ke global NS biar bisa dipanggil di konsol window.videoStore = videoStore; //tambah subscriber buat ngetes videoStore.subscribe(()=>{   console.log('VideoStore:current state', videoStore.getState()); })

Sekarang kita tes dulu koneksi antara StoreReducer & subscribernya. Buka konsol & coba kirim Action ke videoStore:

window.videoStore.dispatch({ type:'HELLO' });

Jadi dari hasil testing di atas, bisa diliat prosesnya:

  1. videoStore menerima Action berupa objek { type: 'HELLO'}
  2. Action diterusin ke videoStateReducer
  3. videoStore jalanin listener/subscriber

Action

Video player ini butuh 6 action (kemungkinan nanti nambah):

  1. SET_SOURCE
  2. PLAY
  3. PAUSE
  4. SEEK
  5. VOLUME_UP
  6. VOLUME_DOWN

Kalo setiap kali butuh Action kita harus bikin objek secara manual pasti repot & resiko error ( typo dsb). Jadi kita bikin yang namanya Action GeneratorAction Factory.

//file: src/actions.js //konstanta buat Action type biar nggak salah ketik export const SET_SOURCE = 'SET_SOURCE'; export const PLAY = 'PLAY'; export const PAUSE = 'PAUSE' export const SEEK = 'SEEK'; export const VOLUME_UP = 'VOLUME_UP'; export const VOLUME_DOWN = 'VOLUME_DOWN'; export const setSource = (url) =>({   type:SET_SOURCE,   url }); export const play = () =>({   type:PLAY }); export const pause = () =>({   type:PAUSE }); export const seek = (time) =>({   type:SEEK,   time }); export const volumeUp = () =>({   type:VOLUME_UP }); export const volumeDown = () =>({   type:VOLUME_DOWN });

Set-Source

Di file src/index.js, kita update videoStateReducer untuk handel action SET_SOURCE.

//file: src/index.js //import dulu semuanya import * as Actions from './actions'; const videoStateReducer = (state = defaultVideoState, action) => {   console.log('VideoStateReducer','state',state,'action', action);   switch(action.type){     case Actions.SET_SOURCE:       return {         ...state,         source: action.url       }   }   return state; };

Di baris paling bawah sendiri, setelah subscribe, kita coba dispatch action SET_SOURCE.

videoStore.subscribe((state)=>{   console.log('VideoStore:current state', videoStore.getState()); }); //DISPATCH ACTION videoStore.dispatch(Actions.setSource('http://google.com'));

Berikutnya, bikin file src/video_wrapper.js untuk logic playernya :

//file: src/video_wrapper.js const VideoWrapper = (store) => {   const videoEl = document.getElementById('myvideo');   //subscribe   store.subscribe(( )=>{     let state = store.getState();     if(state.status === 'new_source'){       videoEl.src = state.source;     }   }); } export default VideoWrapper;

Dan edit dikit Reducer-nya, tambahin status:'new_source'

//file: src/index.js const videoStateReducer = (state = defaultVideoState, action) => {   console.log('VideoStateReducer','state',state,'action', action);   switch(action.type){     case Actions.SET_SOURCE:       return {         ...state,         status:'new_source', //BARIS BARU         source: action.url       }   }   return state; };

Inisialisasi VideoWrapper & coba dispatch action pake URL video:

import VideoWrapper from './video_wrapper'; //dst //init videowrapper VideoWrapper(videoStore); //coba videoStore.dispatch(Actions.setSource('//vjs.zencdn.net/v/oceans.mp4'));

Karena statusnya nanti variatif, biar lebih aman kita bikin konstanta buat status:

//file: status.js export const NEW_SOURCE = 'new_source'; export const PLAY_REQUESTED = 'play_requested'; export const PLAYING = 'playing'; export const PAUSE_REQUESTED = 'pause_requested'; export const PAUSED = 'paused';

Update src/index.js & src/video_wrapper.js, pake konstan di atas:

//file: src/index.js import * as Status from './status'; //dsb //di videoStateReducer(): switch(action.type){   case Actions.SET_SOURCE:     return {       ...state,       status: Status.NEW_SOURCE, //pake konstanta       source: action.url     } } //dsb
//file: src/video_wrapper.js import * as Status from './status'; //dsb   store.subscribe(( )=>{     const state = store.getState();     if(state.status === Status.NEW_SOURCE){       videoEl.src = state.source;     }   }); //dsb

Input UI

Bikin file src/ui/source-input.js.

//file: src/ui/source-input.js import { setSource } from '../actions'; const SourceInput = (store)=>{   const input = document.querySelector('input[name=video-source]');   //default video   input.setAttribute('value','//vjs.zencdn.net/v/oceans.mp4');   const btn = document.getElementById('load-src-btn');   btn.addEventListener('click', (e)=>{     e.preventDefault();     //kalo input nggak kosong,     //dispatch action SET_SOURCE     if(input.value){       store.dispatch(setSource(input.value));     }   }) } export default SourceInput;

Balik ke src/index.js:

//file: src/index.js //import dulu import SourceInput from './ui/source-input'; //dsb //scroll ke bawah ... //init videowrapper VideoWrapper(videoStore); //init SourceInput SourceInput(videoStore); //komen atau hapus baris ini //videoStore.dispatch(Actions.setSource('//vjs.zencdn.net/v/oceans.mp4'));

Play & Pause

Sekarang bikin logic utk play & pause video. Mulai dari reducer.

//file: src/index.js const videoStateReducer = (state = defaultVideoState, action) => {   console.log('VideoStateReducer','state',state,'action', action);   switch(action.type){     case Actions.SET_SOURCE:       return {         ...state,         status: Status.NEW_SOURCE,         source: action.url       }     case Actions.PLAY: // handel action PLAY di sini       return {         ...state,         //update state.status         status: Status.PLAY_REQUESTED       }     case Actions.PAUSE: //handel action PAUSE       return{         ...state,         status: Status.PAUSE_REQUESTED       }   }   return state; };

Update src/video_wrapper.js, tambahin blok else-if untuk handel PLAY_REQUESTED.

store.subscribe(() => {     const state = store.getState();     if (state.status === Status.NEW_SOURCE) {       videoEl.src = state.source;     } else if (state.status === Status.PLAY_REQUESTED) {       //blok if baru utk handle status PLAY_REQUESTED       if (videoEl.src) {         //kalo source udah diset, maenin videonya         videoEl.play();       }     } else if(state.status === Status.PAUSE_REQUESTED){       if(videoEl.src) {         //pause videonya         videoEl.pause();       }     }   });

Play – Pause UI

Lanjutin bikin UI. Tombol Play bukan cuma untuk play, tapi juga dipake untuk pause. Jadi kita bikin file src/ui/playpause-btn.js.

//file: src/ui/playpause-btn.js import * as Status from '../status'; import { play, pause } from '../actions'; const PlayPauseBtn = (store) => {   const btn = document.getElementById('play-pause-btn');   btn.addEventListener('click', (e) => {     e.preventDefault();     const status = store.getState().status;     if (status === Status.PLAYING) {       store.dispatch(pause());     } else {       store.dispatch(play());     }   })   //update label   store.subscribe(() => {     const state = store.getState();     if (state.status === Status.PLAYING) {       btn.innerHTML = 'Pause';     } else if(state.status === Status.PAUSED) {       btn.innerHTML = 'Play';     }   }) }; export default PlayPauseBtn;

Ternyata kita belum punya action untuk update video state ( PLAYING & PAUSE ). Jadi bikin dulu generatornya:

//file: src/actions.js export const VIDEO_STATUS_CHANGED = 'VIDEO_STATUS_CHANGED' //...scroll ke bawah //bikin action generator export const videoStatusChanged = ( status )=>({   type: VIDEO_STATUS_CHANGED,   status })

Nanti yang dispatch action ini adalah VideoWrapper karena dia yang punya elemen <video>, jadi kita update dulu:

//file: src/video_wrapper.js

import * as Status from './status';
//import action yg baru
import { videoStatusChanged } from './actions';

const VideoWrapper = (store) => {
  const videoEl = document.getElementById('myvideo');
  //pasang listener buat 'playing' event
  videoEl.addEventListener('playing', (e) => {
    store.dispatch(videoStatusChanged(Status.PLAYING));
  });
  //pasang listener buat 'pause' event
  videoEl.addEventListener('pause', (e) => {
    store.dispatch(videoStatusChanged(Status.PAUSED));
  });

  // ...dsb

  });

}

export default VideoWrapper;


Time Label
Untuk update time-label ( yang nunjukin waktu sekarang & durasi video), kita bikin dulu actionnya.

//file: src/actions.js

//...dsb
export const UPDATE_TIME = 'UPDATE_TIME';

//... dsb
//scroll ke bawah

export const updateTime = ({time,duration})=>({
  type: UPDATE_TIME,
  time,
  duration
});Copy
Terus update reducer.

const videoStateReducer = (state = defaultVideoState, action) => {
  // ...dsb
  switch(action.type){
    //...dsb
    //bikin case baru buat action UPDATE_TIME
    case Actions.UPDATE_TIME:
      return {
        ...state,
        time: action.time,
        duration: action.duration
      }
  }
  return state;
};Copy
Sekarang bikin modul UI nya.

//file: src/ui/time-ui.js
import * as Status from '../status';

const TimeUI = (store)=>{

  const timeInput = document.querySelector('input[name=time]');
  const duration = document.getElementById('duration');

  store.subscribe(()=>{
    const state = store.getState();
    //update time & duration
    timeInput.value = state.time;
    duration.innerHTML = state.duration;
  })
};

export default TimeUI;

Seek / Scrub
Kenapa time label saya bikin pake <input>? Karena mau saya pake untuk seeking/scrubbing video.

Kayak yg sebelumnya, bikin reducernya dulu:

//file: src/index.js
const videoStateReducer = (state = defaultVideoState, action) => {

  switch(action.type){
    //... dsb
    case Actions.SEEK:
      return{
        ...state,
        status: Status.SEEK_REQUESTED,
        time: action.time
      }
  }
  return state;
};Copy
Bikin juga konstanta buat status nya:

//file: src/status.js
//...dsb
export const SEEK_REQUESTED = 'seek_requested';Copy
Kembali ke modul TimeUI. Kita pake <input> untuk seeking. Jadi kita ketikin mau forward/rewind ke detik ke berapa. Pas input-nya nggak fokus ( focusout) , modul ini kirim action SEEK_REQUESTED.

//file: src/ui/time-ui.js
import * as Status from '../status';
import { seek } from '../actions';

const TimeUI = (store) => {

  //----- HANDEL FOCUS IN & OUT -----//

  //buat simpan value yang lama sebelum fokus
  let valueBeforeFocused = undefined;

  const timeInput = document.querySelector('input[name=time]');
  timeInput.addEventListener('focusin', () => {
    //nilai yang lama sebelum focus
    valueBeforeFocused = timeInput.value;
  });

  timeInput.addEventListener('focusout', () => {
    //kalo value yang baru nggak sama dengan yang lama
    if (timeInput.value != valueBeforeFocused) {
      //dispatch seek action
      store.dispatch(seek(timeInput.value));
    }
  });
  const duration = document.getElementById('duration');

  //... dsb

};

export default TimeUI;Copy
Karena value diupdate secara otomatis selama video berjalan, kita perlu stop update kalo inputnya lagi fokus (focusin). Kita bikin flag isFocused yang nilainya tergantung status focus elemen <input>. Jadi kita update beberapa baris:

//file: src/ui/time-ui.js
import * as Status from '../status';
import { seek } from '../actions';

const TimeUI = (store) => {

  let isFocused = false;

  timeInput.addEventListener('focusin', () => {
    //set isFocused
    isFocused = true;
    //..dsb
  });

  timeInput.addEventListener('focusout', () => {
    //reset isFocused
    isFocused = false;
    //...dsb
  });

  //...dsb

  store.subscribe(() => {
    const state = store.getState();
    //update input value kalo
    //lagi nggak fokus
    if (!isFocused) {
      timeInput.value = state.time;
    }
    duration.innerHTML = state.duration;
  })
};

export default TimeUI;

Tinggalkan Balasan

Alamat email Anda tidak akan dipublikasikan. Ruas yang wajib ditandai *