React useReducer Hook Sinhala Tutorial | useStateට වඩා දියුණු State Management

React useReducer Hook Sinhala Tutorial | useStateට වඩා දියුණු State Management

ආයුබෝවන් React developer කට්ටියට! 👋 අද අපි කතා කරන්න යන්නේ ReactJS වල තියෙන සුපිරිම Hook එකක් ගැන – ඒ තමයි useReducer. අපි හැමෝම useState පාවිච්චි කරලා ඇති, ඒත් සමහර වෙලාවට අපේ application එකේ state management එක ටිකක් සංකීර්ණ වෙද්දි useState පොඩි පොඩි අවුල් ඇති කරනවා. අන්න ඒ වගේ වෙලාවට තමයි useReducer අපේ ගලවාගැනීමේ වීරයා වෙන්නේ!

මේ tutorial එකෙන් අපි useReducer කියන්නේ මොකක්ද, ඒක useState වලට වඩා වෙනස් වෙන්නේ කොහොමද, ඒ වගේම ඒක පාවිච්චි කරන්නේ කොහොමද කියලා හොඳටම ඉගෙන ගන්නවා. practical examples කිහිපයක් එක්ක අපි මේක තේරුම් ගන්න හින්දා බය නැතුව කියවන්න.

useState vs useReducer: කවදාද කුමක් පාවිච්චි කරන්නේ?

මුලින්ම බලමු අපි useState ගැන. අපි දන්නවා useState කියන්නේ React component එකක state එක manage කරන්න තියෙන සරලම සහ ජනප්‍රියම Hook එක. පොඩි පොඩි state values (string, number, boolean) වෙනස් කරන්න, එක එක state variable එකක් තනි තනියම update කරන්න useState නියමයි.

import React, { useState } from 'react';

function CounterWithUseState() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(count - 1)}>Decrement</button>
    </div>
  );
}

හැබැයි, state එක සංකීර්ණ වෙද්දි, උදාහරණයක් විදියට, ඔබ user object එකක් manage කරනවා නම්, ඒකේ property ගොඩක් තියෙනවා නම්, එහෙම නැත්නම් එක state update එකකින් state values ගොඩක් වෙනස් කරන්න ඕන නම්, useState ටිකක් අවුල් සහගත වෙන්න පුළුවන්. ඒ වගේම එකම තැනක state updates ගොඩක් තියෙනවා නම්, ඒක කියවන්න අමාරු වෙනවා, maintain කරන්නත් අපහසු වෙනවා.

අන්න ඒ වගේ වෙලාවට තමයි useReducer එන්නේ. useReducer කියන්නේ සංකීර්ණ state logic තියෙන අවස්ථා වලට වඩා හොඳ විසඳුමක්. Redux වගේ state management library එකක් පාවිච්චි කරලා පුරුදු අයට useReducer එකේ concepts හරිම හුරුපුරුදු වෙයි. මොකද මේ දෙකේම core idea එක ගොඩක් සමානයි.

useReducer පාවිච්චි කරන්නේ ප්‍රධාන වශයෙන් හේතු දෙකක් නිසා:

  • Complex State Logic: State එක update කරන logic එක ටිකක් සංකීර්ණ නම්, conditional logic ගොඩක් තියෙනවා නම්.
  • Multiple Related State Updates: එක action එකකින් state එකේ කොටස් කිහිපයක් එකපාර update කරන්න ඕන නම්.

useReducer වල මූලික සංකල්ප (Core Concepts)

useReducer වැඩ කරන්නේ ප්‍රධාන අංග තුනක් මතයි:

  1. Reducer Function: මේක තමයි අපේ state එක වෙනස් කරන "හදවත". මේ function එකට දැනට තියෙන state එකයි, මොකක්ද කරන්න ඕන action එකයි (ක්‍රියාව) ලැබෙනවා. ඊට පස්සේ ඒ action එකට අනුව අලුත් state එකක් return කරනවා.
  2. Initial State: Component එක render වෙද්දි අපේ state එක මුලින්ම පටන් ගන්න ඕන මොන අගයකින්ද කියන එක තමයි initial state එක.
  3. Dispatch Function: State එක වෙනස් කරන්න ඕන වෙලාවට අපි මේ dispatch function එකට action එකක් (object එකක්) යවනවා. මේ dispatch function එක තමයි reducer function එක trigger කරන්නේ.

මේක තවදුරටත් පැහැදිලි කරන්න මේ equation එක බලන්න:

(state, action) => newState

  • state: දැනට තියෙන state එක.
  • action: state එක වෙනස් කරන්න ඕන මොකක්ද කියලා කියන object එක. සාමාන්‍යයෙන් මේකේ type කියන property එකක් තියෙනවා, ඒකෙන් කියනවා මොන action එකද සිද්ධ වෙන්න ඕන කියලා. ඒ වගේම payload කියන property එකකින් අදාළ දත්තත් යවන්න පුළුවන්.
  • newState: action එක run වුණාට පස්සේ හැදෙන අලුත් state එක.

useReducer එකේ Syntax එක

const [state, dispatch] = useReducer(reducer, initialState, init);
  • state: reducer එකෙන් return කරන current state එක.
  • dispatch: action එකක් trigger කරන්න පාවිච්චි කරන function එක.
  • reducer: state එක වෙනස් කරන logic එක තියෙන function එක.
  • initialState: state එකේ ආරම්භක අගය.
  • init (optional): මේක optional argument එකක්. Initial state එක lazy initialize කරන්න පාවිච්චි කරනවා.

ප්‍රායෝගික උදාහරණය 1: Counter Application එකක් useReducer වලින්

අපි මුලින්ම useState වලින් හදපු counter එක useReducer වලට refactor කරමු. එතකොට වෙනස හොඳට තේරෙයි.

මුලින්ම Reducer Function එක හදමු

// counterReducer.js (වෙනම file එකක් විදියට තියාගන්නත් පුළුවන්)
function counterReducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    case 'RESET':
      return { count: 0 };
    default:
      throw new Error();
  }
}

මේක තමයි අපේ reducer function එක. මේක state එකයි action එකයි දෙකම ගන්නවා. action.type එක අනුව, ඒ කියන්නේ කරන්න ඕන ක්‍රියාව අනුව, අලුත් state එකක් return කරනවා. වැදගත්ම දේ තමයි reducer function එක pure function එකක් වෙන්න ඕන. ඒ කියන්නේ ඒක side effects ඇති කරන්න එපා, ඒ වගේම හැම වෙලාවෙම අලුත් state object එකක් return කරන්න ඕන (exist වන state object එක modify කරන්න එපා).

useReducer පාවිච්චි කරලා Counter Component එක හදමු

import React, { useReducer } from 'react';

// Reducer function එක component එකෙන් පිටත තියාගන්න එක හොඳ practice එකක්.
function counterReducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    case 'RESET':
      return { count: action.payload || 0 }; // payload එකක් තිබ්බොත් ඒකෙන් reset කරන්න
    default:
      throw new Error(`Unhandled action type: ${action.type}`);
  }
}

const initialState = { count: 0 }; // Initial State එක

function CounterWithUseReducer() {
  const [state, dispatch] = useReducer(counterReducer, initialState);

  return (
    <div>
      <h3>Counter with useReducer</h3>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button>
      <button onClick={() => dispatch({ type: 'RESET', payload: 10 })}>Reset to 10</button>
      <button onClick={() => dispatch({ type: 'RESET' })}>Reset to 0</button> {/* payload නැතුව 0 ට reset කරන්නත් පුළුවන් */}
    </div>
  );
}

export default CounterWithUseReducer;

මේ code එක දිහා බලන්න, dispatch function එකට අපි object එකක් යවනවා. මේ object එක තමයි action එක. ඒකේ type property එකෙන් කියනවා මොකක්ද කරන්න ඕන ක්‍රියාව කියලා. RESET action එකට අපි payload එකක් යවපු විදියත් බලන්න. ඒකෙන් පුළුවන් අමතර දත්ත reducer එකට යවන්න.

මේ විදියට state logic එක component එකෙන් වෙන් කරලා reducer function එකක් ඇතුලට දාන එකෙන් code එක maintain කරන්න පහසු වෙනවා. ඒ වගේම debugging කරන්නත් ලේසියි.

ප්‍රායෝගික උදාහරණය 2: සංකීර්ණ Multi-Step Form එකක් useReducer වලින්

හිතන්න, ඔබට multi-step form එකක් තියෙනවා කියලා. ඒකේ user details, address details, payment details වගේ steps කිහිපයක් තියෙනවා. හැම step එකකම data වෙන වෙනම manage කරන්න useState පාවිච්චි කරනවා නම්, component එක හරිම අවුල් වෙයි. අන්න ඒ වගේ වෙලාවට useReducer තමයි නියම solution එක.

අපි මේ form එකේ state එකට මුලින්ම initial state එකක් හදමු.

const initialFormState = {
  currentStep: 1,
  formData: {
    name: '',
    email: '',
    address: '',
    city: '',
    cardNumber: '',
    expiryDate: ''
  },
  errors: {}
};

ඊට පස්සේ reducer function එක හදමු.

function formReducer(state, action) {
  switch (action.type) {
    case 'NEXT_STEP':
      return {
        ...state,
        currentStep: state.currentStep + 1
      };
    case 'PREV_STEP':
      return {
        ...state,
        currentStep: state.currentStep - 1
      };
    case 'UPDATE_FIELD':
      return {
        ...state,
        formData: {
          ...state.formData,
          [action.field]: action.value
        }
      };
    case 'SET_ERRORS':
      return {
        ...state,
        errors: action.errors
      };
    case 'RESET_FORM':
      return initialFormState; // මුල් state එකටම යනවා
    default:
      throw new Error(`Unhandled action type: ${action.type}`);
  }
}

මේ reducer function එකේ බලන්න, එක action එකකින් (UPDATE_FIELD) අපිට formData object එකේ ඕනෑම field එකක් update කරන්න පුළුවන්. ඒ වගේම NEXT_STEP, PREV_STEP වගේ actions වලින් current step එක manage කරන්නත් පුළුවන්. මේ හැම action එකකින්ම state එකේ වෙනස් වෙන කොටස් අලුත් object එකක විදියට return කරනවා.

Component එකේ useReducer පාවිච්චි කරන හැටි

import React, { useReducer } from 'react';

// reducer function එකයි initial state එකයි උඩින් තියෙනවා කියලා හිතමු

function MultiStepForm() {
  const [state, dispatch] = useReducer(formReducer, initialFormState);

  const handleInputChange = (e) => {
    const { name, value } = e.target;
    dispatch({ type: 'UPDATE_FIELD', field: name, value });
  };

  const handleNext = () => {
    // Validate current step before moving to next
    // If valid:
    dispatch({ type: 'NEXT_STEP' });
  };

  const handlePrevious = () => {
    dispatch({ type: 'PREV_STEP' });
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('Form submitted:', state.formData);
    dispatch({ type: 'RESET_FORM' });
    alert('Form submitted successfully!');
  };

  const renderStep = () => {
    switch (state.currentStep) {
      case 1:
        return (
          <div>
            <h3>Step 1: Personal Details</h3>
            <label>Name:</label>
            <input type="text" name="name" value={state.formData.name} onChange={handleInputChange} /><br />
            <label>Email:</label>
            <input type="email" name="email" value={state.formData.email} onChange={handleInputChange} /><br />
            <button onClick={handleNext}>Next</button>
          </div>
        );
      case 2:
        return (
          <div>
            <h3>Step 2: Address Details</h3>
            <label>Address:</label>
            <input type="text" name="address" value={state.formData.address} onChange={handleInputChange} /><br />
            <label>City:</label>
            <input type="text" name="city" value={state.formData.city} onChange={handleInputChange} /><br />
            <button onClick={handlePrevious}>Previous</button>
            <button onClick={handleNext}>Next</button>
          </div>
        );
      case 3:
        return (
          <div>
            <h3>Step 3: Payment Details</h3>
            <label>Card Number:</label>
            <input type="text" name="cardNumber" value={state.formData.cardNumber} onChange={handleInputChange} /><br />
            <label>Expiry Date:</label>
            <input type="text" name="expiryDate" value={state.formData.expiryDate} onChange={handleInputChange} /><br />
            <button onClick={handlePrevious}>Previous</button>
            <button type="submit">Submit</button>
          </div>
        );
      default:
        return <h3>Form Completed!</h3>;
    }
  };

  return (
    <div style={{ border: '1px solid #ccc', padding: '20px', margin: '20px', borderRadius: '8px' }}>
      <h2>Multi-Step Form with useReducer</h2>
      <form onSubmit={handleSubmit}>
        {renderStep()}
        {/* Optional: Error display */}
        {Object.keys(state.errors).length > 0 && (
          <div style={{ color: 'red', marginTop: '10px' }}>
            <p>Please fix the following errors:</p>
            <ul>
              {Object.entries(state.errors).map(([key, value]) => (
                <li key={key}>{key}: {value}</li>
              ))}
            </ul>
          </div>
        )}
      </form>
    </div>
  );
}

export default MultiStepForm;

මේ උදාහරණයෙන් පේනවා useReducer කොච්චර ප්‍රයෝජනවත්ද කියලා සංකීර්ණ form එකක state එක manage කරන්න. හැම input එකකම වෙනස් වීමක් UPDATE_FIELD action එකකින් handle කරනවා. ඒ වගේම step වෙනස් කරන්නත් වෙනම actions තියෙනවා. මේකෙන් component එක ඇතුළත state logic එක ගොඩක් clean වෙනවා.

හොඳම පුරුදු සහ පොදු ගැටළු (Best Practices & Common Issues)

Reducer Functions පිරිසිදුව තියාගැනීම (Pure Functions)

ගොඩක් වැදගත් දෙයක් තමයි reducer function එක හැම වෙලාවෙම pure function එකක් වෙන්න ඕන. ඒ කියන්නේ:

  • එකම inputs වලට හැම වෙලාවෙම එකම output එක දෙන්න ඕන.
  • State එක direct modify කරන්න එපා. අලුත් state object එකක් return කරන්න.
  • Side effects (API calls, browser storage changes, etc.) reducer එක ඇතුළත කරන්න එපා. ඒව component එක ඇතුළේ useEffect වගේ Hooks පාවිච්චි කරලා කරන්න.
// වැරදි විදිය (State එක mutate කරනවා)
function badReducer(state, action) {
  if (action.type === 'ADD_ITEM') {
    state.items.push(action.item); // මේක state එක mutate කරනවා!
    return state;
  }
  return state;
}

// නිවැරදි විදිය (අලුත් object එකක් return කරනවා)
function goodReducer(state, action) {
  if (action.type === 'ADD_ITEM') {
    return {
      ...state,
      items: [...state.items, action.item] // අලුත් array එකක් හදලා return කරනවා
    };
  }
  return state;
}

dispatch Actions අමතක වීම

State එක වෙනස් කරන්න ඕන හැම වෙලාවකම dispatch function එක පාවිච්චි කරන්න අමතක කරන්න එපා. direct state එක update කරන්න බැහැ, useState වගේ.

useReducer සහ useContext එකට පාවිච්චි කිරීම

ඔබේ application එකේ state එක සංකීර්ණ නම් සහ ඒ state එක components ගොඩක් අතර share කරන්න ඕන නම්, useReducer එක useContext එක්ක පාවිච්චි කරන්න පුළුවන්. මේක Redux වගේ library එකක functionality එකට ටිකක් සමානයි, නමුත් React ecosystem එක ඇතුළතින්ම. useContext වලින් dispatch function එක සහ state එක components වලට provide කරලා, ඒ components වලට props drill කරන්නේ නැතුව ඒවට access කරන්න පුළුවන්.

Debugging පහසුව

useReducer පාවිච්චි කරන එකේ තවත් ලොකු වාසියක් තමයි debugging පහසු වෙන එක. මොකද හැම state වෙනස් වීමක්ම action එකක් හරහා යන නිසා, state එක කොහොමද වෙනස් වුණේ කියලා trace කරන්න ලේසියි. Redux DevTools වගේ tools වලටත් සමාන DevTools React වලටත් තියෙනවා useReducer actions inspect කරන්න පුළුවන්.

අවසන් වචන (Conclusion)

ඉතින් යාලුවනේ, ඔයාලට දැන් useReducer Hook එක ගැන හොඳ අවබෝධයක් ලැබෙන්න ඇති කියලා හිතනවා. useState කියන්නේ සරල state management වලට නියම වුණත්, application එකේ state logic එක සංකීර්ණ වෙද්දි useReducer කියන්නේ ඉතාම බලවත් සහ පිළිවෙලකට state manage කරන්න පුළුවන් tool එකක්.

මේක React ecosystem එකේ තියෙන "mini-Redux" එකක් වගේ ක්‍රියා කරන නිසා, Redux ගැන අලුතෙන් ඉගෙන ගන්න අයටත් මේක හොඳ පදනමක් වෙයි. ප්‍රධානම දේවල් මතක තියාගන්න: reducer function එක pure වෙන්න ඕන, state එක mutate කරන්න එපා, හැම වෙලාවෙම අලුත් state object එකක් return කරන්න.

ඔයාලගේ ඊලඟ React project එකේදී useReducer පාවිච්චි කරලා බලන්න. අලුත් දෙයක් ඉගෙන ගත්තම ඒක practice කරන එක ගොඩක් වැදගත්. මේ ගැන ඔයාලගේ අත්දැකීම්, ප්‍රශ්න පහළ comment section එකේ කියන්න! හැමෝටම ජය!