रेड्यूसर में State Logic निकालना

कई Event Handlers में फैले कई State logic वाले Event भारी पड़ सकते हैं। इन मामलों के लिए, आप अपने Event के बाहर सभी State Update Logic को एक एकल फ़ंक्शन में संघटित (consolidate) कर सकते हैं, जिसे रेड्यूसर कहा जाता है।

You will learn

  • रिड्यूसर फ़ंक्शन क्या है
  • useState को useReducer में Re-factor कैसे करें
  • रेड्यूसर का उपयोग कब करें
  • एक अच्छा कैसे लिखें

रिड्यूसर (Reducer) के साथ State Logic को संघटित करें

जैसे-जैसे आपके Components की जटिलता बढ़ती है, किसी Component की स्थिति को update करने के सभी अलग-अलग तरीकों को एक नज़र में देखना कठिन हो जाता है। उदाहरण के लिए, नीचे दिया गया TaskApp component state में tasks की एक सरणी (array) रखता है और कार्यों को जोड़ने, हटाने और संपादित (edit) करने के लिए तीन अलग-अलग ईवेंट हैंडलर का उपयोग करता है:

import { useState } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';

export default function TaskApp() {
  const [tasks, setTasks] = useState(initialTasks);

  function handleAddTask(text) {
    setTasks([
      ...tasks,
      {
        id: nextId++,
        text: text,
        done: false,
      },
    ]);
  }

  function handleChangeTask(task) {
    setTasks(
      tasks.map((t) => {
        if (t.id === task.id) {
          return task;
        } else {
          return t;
        }
      })
    );
  }

  function handleDeleteTask(taskId) {
    setTasks(tasks.filter((t) => t.id !== taskId));
  }

  return (
    <>
      <h1>Prague itinerary</h1>
      <AddTask onAddTask={handleAddTask} />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

let nextId = 3;
const initialTasks = [
  {id: 0, text: 'Visit Kafka Museum', done: true},
  {id: 1, text: 'Watch a puppet show', done: false},
  {id: 2, text: 'Lennon Wall pic', done: false},
];

इसका प्रत्येक इवेंट हैंडलर State को Update करने के लिए setTasks को कॉल करता है। जैसे-जैसे यह Component बढ़ता है, वैसे-वैसे इसमें State Logic की मात्रा भी बढ़ती जाती है। इस जटिलता को कम करने और अपने सभी तर्कों को एक आसान पहुंच वाले स्थान पर रखने के लिए, आप उस ‘state logic’ को अपने component के बाहर एक single फ़ंक्शन में ले जा सकते हैं, जिसे “रेड्यूसर” कहा जाता है।

रेड्यूसर State को संभालने का एक अलग तरीका है। आप तीन चरणों में useState से useReducer पर माइग्रेट कर सकते हैं:

  1. सेटिंग स्थिति से प्रेषण क्रियाओं (dispatching action) के लिए Move
  2. एक रेड्यूसर फ़ंक्शन के लिए Write
  3. अपने Component से रेड्यूसर का Use करें।

चरण 1: सेटिंग स्थिति से प्रेषण क्रियाओं की ओर बढ़ें :

आपके ईवेंट हैंडलर वर्तमान में निर्दिष्ट करते हैं setting state द्वारा क्या करें :

function handleAddTask(text) {
setTasks([
...tasks,
{
id: nextId++,
text: text,
done: false,
},
]);
}

function handleChangeTask(task) {
setTasks(
tasks.map((t) => {
if (t.id === task.id) {
return task;
} else {
return t;
}
})
);
}

function handleDeleteTask(taskId) {
setTasks(tasks.filter((t) => t.id !== taskId));
}

सभी setting state तर्क हटाएँ। आपके पास तीन इवेंट हैंडलर बचे हैं:

  • जब User “Add” handleAddTask(text) कॉल किया जाता है .
  • जब User “Save” handleChangeTask(task) कॉल किया जाता है .
  • जब User “Delete” दबाता है तो handleDeleteTask(taskId) कॉल किया जाता है .

Reducer के साथ Setting State का management सीधे State की स्थापना से थोड़ा अलग है।

function handleAddTask(text) {
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}

function handleChangeTask(task) {
dispatch({
type: 'changed',
task: task,
});
}

function handleDeleteTask(taskId) {
dispatch({
type: 'deleted',
id: taskId,
});
}

जिस ऑब्जेक्ट को आप dispatch के लिए पास करते हैं उसे “action” कहा जाता है:

function handleDeleteTask(taskId) {
dispatch(
// "action" object:
{
type: 'deleted',
id: taskId,
}
);
}

यह एक नियमित जावास्क्रिप्ट ऑब्जेक्ट है। आप तय करें कि इसमें क्या डालना है, लेकिन आम तौर पर इसमें what happened के बारे में न्यूनतम जानकारी होनी चाहिए। (आप बाद के चरण में dispatch फ़ंक्शन स्वयं जोड़ देंगे।)

Note

Action object का कोई भी आकार हो सकता है।

परंपरा के अनुसार, इसे एक स्ट्रिंग type देना आम बात है जो बताती है कि क्या हुआ, और अन्य क्षेत्रों में कोई अतिरिक्त जानकारी भेजती है। ‘type’ एक event के लिए विशिष्ट है, इसलिए इस उदाहरण में या तो ‘added’ या ‘added_task’ ठीक रहेगा। ऐसा नाम चुनें जो कहे कि क्या हुआ!

dispatch({
// specific to component
type: 'what_happened',
// other fields go here
});

Step 2: एक रिड्यूसर फ़ंक्शन लिखें :

रिड्यूसर फ़ंक्शन वह जगह है जहां आप अपना State logic डालेंगे। यह दो arguments लेता है, current state और action object, और यह अगली state लौटाता है:

function yourReducer(state, action) {
// return next state for React to set
}

React उस state को सेट कर देगा जो आप रिड्यूसर से लौटाते हैं।

इस उदाहरण में अपने state setting logic को अपने ईवेंट हैंडलर से रेड्यूसर फ़ंक्शन में स्थानांतरित करने के लिए, आप यह करेंगे:

  1. cureent state (‘tasks’) को first argument के रूप में घोषित (declare) करें।
  2. ‘action’ ऑब्जेक्ट को second argument के रूप में घोषित करें।
  3. रिड्यूसर से next state लौटाएं (जो React स्थिति को सेट करेगा)।

यहां सभी setting state logic एक reducer फ़ंक्शन में माइग्रेट किए गए हैं:

function tasksReducer(tasks, action) {
if (action.type === 'added') {
return [
...tasks,
{
id: action.id,
text: action.text,
done: false,
},
];
} else if (action.type === 'changed') {
return tasks.map((t) => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
} else if (action.type === 'deleted') {
return tasks.filter((t) => t.id !== action.id);
} else {
throw Error('Unknown action: ' + action.type);
}
}

क्योंकि रेड्यूसर फ़ंक्शन राज्य (tasks) को एक argument के रूप में लेता है, आप इसे अपने event के बाहर घोषित - declare कर सकते हैं। इससे indentatation स्तर कम हो जाता है और आपके कोड को पढ़ना आसान हो सकता है।

Note

उपरोक्त कोड if/else स्टेटमेंट का उपयोग करता है, लेकिन यह रिड्यूसर के अंदर स्विच स्टेटमेंट का उपयोग करने की परंपरा है। परिणाम वही है, लेकिन स्विच स्टेटमेंट को एक नज़र में पढ़ना आसान हो सकता है।

हम इस दस्तावेज़ के शेष भाग में उनका उपयोग इस प्रकार करेंगे:

function tasksReducer(tasks, action) {
switch (action.type) {
case 'added': {
return [
...tasks,
{
id: action.id,
text: action.text,
done: false,
},
];
}
case 'changed': {
return tasks.map((t) => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
}
case 'deleted': {
return tasks.filter((t) => t.id !== action.id);
}
default: {
throw Error('Unknown action: ' + action.type);
}
}
}

हम प्रत्येक case ब्लॉक को { and } curly braces में लपेटने की सलाह देते हैं ताकि अलग-अलग case के अंदर घोषित वेरिएबल एक-दूसरे से न टकराएं। इसके अलावा, एक case आमतौर पर return के साथ समाप्त होना चाहिए। यदि आप return करना भूल जाते हैं, तो कोड अगले case में “fall through”, जिससे गलतियाँ हो सकती हैं!

यदि आप अभी तक स्विच स्टेटमेंट के साथ सहज नहीं हैं, तो if/else का उपयोग करना पूरी तरह से ठीक है।

Deep Dive

State 2: रिड्यूसर को इस तरह क्यों callकिया जाता है?

हालाँकि रिड्यूसर आपके component के अंदर कोड की मात्रा को “reduce” कर सकते हैं, वास्तव में उनका नाम reduce() के नाम पर रखा गया है। Global_Objects/Array/Reduce) ऑपरेशन जिसे आप arrays पर निष्पादित(perform) कर सकते हैं।

reduce() ऑपरेशन आपको एक array लेने और कई में से एक मान को “accumulate” करने की सुविधा देता है:

const arr = [1, 2, 3, 4, 5];
const sum = arr.reduce(
(result, number) => result + number
); // 1 + 2 + 3 + 4 + 5

जिस फ़ंक्शन को आप reduce करने के लिए पास करते हैं उसे “reducer” के रूप में जाना जाता है। यह अब तक का परिणाम और current item लेता है, फिर अगला परिणाम लौटाता है। React reducer उसी Idea का एक उदाहरण है: वे अब state so far और action लेते हैं, और अगली state वापस करते हैं। इस तरह से , वे समय के साथ actions को state में जमा करते हैं।

आप अपने रेड्यूसर फ़ंक्शन को पास करके अंतिम स्थिति की गणना करने के लिए initialState और actions की एक सरणी के साथ reduce() विधि का भी उपयोग कर सकते हैं:

import tasksReducer from './tasksReducer.js';

let initialState = [];
let actions = [
  {type: 'added', id: 1, text: 'Visit Kafka Museum'},
  {type: 'added', id: 2, text: 'Watch a puppet show'},
  {type: 'deleted', id: 1},
  {type: 'added', id: 3, text: 'Lennon Wall pic'},
];

let finalState = actions.reduce(tasksReducer, initialState);

const output = document.getElementById('output');
output.textContent = JSON.stringify(finalState, null, 2);

आपको संभवतः इसे स्वयं करने की आवश्यकता नहीं होगी, लेकिन यह React के समान है!

Step 3: अपने component से Reducer का उपयोग करें

अंत में, आपको tasksReducer को अपने component से जोड़ना होगा। React से useReducer Hook Import करें:

import { useReducer } from 'react';

फिर आप useState को प्रतिस्थापित (replace) कर सकते हैं:

const [tasks, setTasks] = useState(initialTasks);

with useReducer like so:

const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

useReducer hook useState के समान है - आपको इसे initial state में पास करना होगा और यह एक stateful value और set state करने का एक तरीका देता है (इस मामले में, dispatch फ़ंक्शन)। लेकिन यह थोड़ा अलग है.

useReducer hook two aruguments लेता है:

  1. एक रेड्यूसर फ़ंक्शन
  2. एक initial state

और यह लौटाता है:

  1. एक stateful value
  2. एक dispatch फ़ंक्शन (रेड्यूसर को user actions को “dispatch” करने के लिए)

अब यह पूरी तरह से व्यवस्थित हो गया है! यहां, रेड्यूसर को component फ़ाइल के नीचे घोषित किया गया है:

import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';

export default function TaskApp() {
  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

  function handleAddTask(text) {
    dispatch({
      type: 'added',
      id: nextId++,
      text: text,
    });
  }

  function handleChangeTask(task) {
    dispatch({
      type: 'changed',
      task: task,
    });
  }

  function handleDeleteTask(taskId) {
    dispatch({
      type: 'deleted',
      id: taskId,
    });
  }

  return (
    <>
      <h1>Prague itinerary</h1>
      <AddTask onAddTask={handleAddTask} />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [
        ...tasks,
        {
          id: action.id,
          text: action.text,
          done: false,
        },
      ];
    }
    case 'changed': {
      return tasks.map((t) => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter((t) => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

let nextId = 3;
const initialTasks = [
  {id: 0, text: 'Visit Kafka Museum', done: true},
  {id: 1, text: 'Watch a puppet show', done: false},
  {id: 2, text: 'Lennon Wall pic', done: false},
];

यदि आप चाहें, तो आप रिड्यूसर को किसी भिन्न फ़ाइल में भी ले जा सकते हैं:

import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import tasksReducer from './tasksReducer.js';

export default function TaskApp() {
  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

  function handleAddTask(text) {
    dispatch({
      type: 'added',
      id: nextId++,
      text: text,
    });
  }

  function handleChangeTask(task) {
    dispatch({
      type: 'changed',
      task: task,
    });
  }

  function handleDeleteTask(taskId) {
    dispatch({
      type: 'deleted',
      id: taskId,
    });
  }

  return (
    <>
      <h1>Prague itinerary</h1>
      <AddTask onAddTask={handleAddTask} />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

let nextId = 3;
const initialTasks = [
  {id: 0, text: 'Visit Kafka Museum', done: true},
  {id: 1, text: 'Watch a puppet show', done: false},
  {id: 2, text: 'Lennon Wall pic', done: false},
];

जब आप इस तरह की concerns को अलग करते हैं तो component logic को पढ़ना आसान हो सकता है। अब ईवेंट हैंडलर केवल कार्रवाई भेजकर निर्दिष्ट करते हैं कि क्या हुआ, और रिड्यूसर फ़ंक्शन यह निर्धारित करता है कि उनके जवाब में state कैसे अपडेट होता है।

तुलना करे useState और useReducer के बिच

रेड्यूसर कमियों से रहित नहीं हैं! यहां कुछ तरीके दिए गए हैं जिनसे आप उनकी तुलना कर सकते हैं:

  • Code Size: आम तौर पर, useState के साथ आपको पहले कम कोड लिखना होता है। useReducer के साथ, आपको रेड्यूसर फ़ंक्शन और डिस्पैच क्रियाएं दोनों लिखनी होंगी। हालाँकि, यदि कई ईवेंट हैंडलर इसी तरह से स्थिति को संशोधित करते हैं, तो useReducer कोड को कम करने में मदद कर सकता है।

  • Readability: state अपडेट सरल होने पर useState को पढ़ना बहुत आसान है। जब वे अधिक जटिल हो जाते हैं, तो वे आपके component के कोड को फूला सकते हैं और स्कैन करना कठिन बना सकते हैं। इस मामले में, useReducer आपको अपडेट लॉजिक के how को इवेंट हैंडलर के what happened से स्पष्ट रूप से अलग करने देता है।

  • Debugging: जब आपके पास useState के साथ कोई बग होता है, तो यह बताना मुश्किल हो सकता है कि where State गलत तरीके से सेट की गई थी, और whyuseReducer के साथ, आप प्रत्येक State अपडेट को देखने के लिए अपने रेड्यूसर में एक कंसोल लॉग जोड़ सकते हैं, और क्यों ऐसा हुआ (किस action के कारण)। यदि प्रत्येक action सही है, तो आपको पता चल जाएगा कि गलती रिड्यूसर लॉजिक में ही है। हालाँकि, आपको useState की तुलना में अधिक कोड से गुजरना होगा।

  • Testing रिड्यूसर एक शुद्ध फ़ंक्शन है जो आपके component पर निर्भर नहीं करता है। इसका मतलब है कि आप इसे अलग से import और परीक्षण कर सकते हैं। जबकि आम तौर पर अधिक यथार्थवादी वातावरण में event का परीक्षण करना सबसे अच्छा होता है, complex state update logic के लिए यह दावा करना उपयोगी हो सकता है कि आपका रेड्यूसर एक विशेष initial state और कार्रवाई के लिए एक विशेष state लौटाता है।

  • व्यक्तिगत प्राथमिकता: कुछ लोगों को रिड्यूसर पसंद होते हैं, अन्य को नहीं। वह ठीक है। यह प्राथमिकता का मामला है. आप हमेशा useState और useReducer के बीच आगे और पीछे कनवर्ट कर सकते हैं: वे समकक्ष हैं!

यदि आप किसी घटक में गलत स्थिति अपडेट के कारण अक्सर बग का सामना करते हैं, और इसके कोड में अधिक संरचना जोड़ना चाहते हैं, तो हम रेड्यूसर का उपयोग करने की सलाह देते हैं। आपको हर चीज़ के लिए रिड्यूसर का उपयोग करने की ज़रूरत नहीं है: बेझिझक मिश्रण और मिलान करें! आप एक ही घटक में useState और useReducer भी कर सकते हैं।

रिड्यूसर अच्छी तरह से लिखना

रिड्यूसर लिखते समय इन दो युक्तियों को ध्यान में रखें:

  • रेड्यूसर शुद्ध होने चाहिए। स्टेट अपडेटर फ़ंक्शंस के समान, रेंडरिंग के दौरान रेड्यूसर चलते हैं! (अगले रेंडर तक क्रियाएं कतारबद्ध हैं।) इसका मतलब है कि रिड्यूसर [शुद्ध होना चाहिए] (/ सीखें/रख-घटकों-शुद्ध) - समान इनपुट का परिणाम हमेशा समान आउटपुट होता है। उन्हें अनुरोध नहीं भेजना चाहिए, टाइमआउट शेड्यूल नहीं करना चाहिए, या कोई साइड इफेक्ट (ऑपरेशन जो घटक के बाहर की चीजों को प्रभावित करते हैं) नहीं करना चाहिए। उन्हें उत्परिवर्तन के बिना [objects] (/ सीखें/अपडेट-ऑब्जेक्ट्स-इन-स्टेट) और [array] (/ सीखें/update-array-in-state) को अपडेट करना चाहिए।
  • ** प्रत्येक क्रिया एक single user इंटरैक्शन का वर्णन करती है, भले ही इससे डेटा में कई परिवर्तन होते हों। ** उदाहरण के लिए, यदि कोई user रिड्यूसर द्वारा प्रबंधित पांच फ़ील्ड वाले फॉर्म पर “रीसेट” दबाता है, तो यह अधिक समझ में आता है पाँच अलग-अलग set_field क्रियाओं के बजाय एक reset_form क्रिया भेजें। यदि आप प्रत्येक क्रिया को रेड्यूसर में लॉग करते हैं, तो वह लॉग इतना स्पष्ट होना चाहिए कि आप यह बता सकें कि किस क्रम में क्या इंटरैक्शन या प्रतिक्रियाएं हुईं। यह डिबगिंग में मदद करता है!

इमर के साथ संक्षिप्त रिड्यूसर लिखना

ठीक वैसे ही जैसे [ऑब्जेक्ट्स को अपडेट करना](/सीखना/अपडेट करना-ऑब्जेक्ट्स-इन-स्टेट#लिखना-संक्षिप्त-अपडेट-लॉजिक-विथ-इमर) और [array]( /सीखना/अपडेट करना-सरणी-इन-स्टेट#लिखना-संक्षिप्त) -update-logic-with-immer) नियमित स्थिति में, आप रिड्यूसर को अधिक संक्षिप्त बनाने के लिए Immer लाइब्रेरी का उपयोग कर सकते हैं। यहां, useImmerReducer आपको push या arr[i] = असाइनमेंट के साथ स्थिति को बदलने की सुविधा देता है:

{
  "dependencies": {
    "immer": "1.7.3",
    "react": "latest",
    "react-dom": "latest",
    "react-scripts": "latest",
    "use-immer": "0.5.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  },
  "devDependencies": {}
}

रेड्यूसर शुद्ध होने चाहिए, इसलिए उन्हें स्थिति में परिवर्तन नहीं करना चाहिए। लेकिन इमर आपको एक विशेष ‘draft’ ऑब्जेक्ट प्रदान करता है जिसे बदलना सुरक्षित है। हुड के तहत, इमर आपके द्वारा ‘ड्राफ्ट’ में किए गए परिवर्तनों के साथ आपके state की एक प्रति बनाएगा। यही कारण है कि useImmerReducer द्वारा प्रबंधित रिड्यूसर अपने पहले loguc को बदल सकते हैं और उन्हें स्थिति वापस करने की आवश्यकता नहीं है।

Recap

  • useState से useReducer में कनवर्ट करने के लिए:

    1. इवेंट हैंडलर्स से कार्रवाई भेजना।
    2. एक रिड्यूसर फ़ंक्शन लिखें जो किसी दिए गए state और क्रिया के लिए अगला state लौटाता है।
    3. useState को useReducer से बदलें।
  • रेड्यूसर के लिए आपको थोड़ा अधिक कोड लिखने की आवश्यकता होती है, लेकिन वे डिबगिंग और परीक्षण में मदद करते हैं।

  • रिड्यूसर शुद्ध होने चाहिए.

  • प्रत्येक क्रिया एकल उपयोगकर्ता इंटरैक्शन का वर्णन करती है।

  • यदि आप परिवर्तनशील शैली में रिड्यूसर लिखना चाहते हैं तो इमर का उपयोग करें।

Challenge 1 of 4:
ईवेंट संचालकों से कार्रवाई भेजना

वर्तमान में, ContactList.js और Chat.js में इवेंट हैंडलर के पास // TODO टिप्पणियाँ हैं। यही कारण है कि इनपुट में टाइप करना काम नहीं करता है, और बटन पर क्लिक करने से चयनित प्राप्तकर्ता नहीं बदलता है।

संबंधित क्रियाओं को dispatch करने के लिए इन दो // TODO को कोड से बदलें। अपेक्षित आकार और क्रियाओं के प्रकार को देखने के लिए, messengerReducer.js में रिड्यूसर की जाँच करें। रिड्यूसर पहले से ही लिखा हुआ है इसलिए आपको इसे बदलने की आवश्यकता नहीं होगी। आपको केवल ContactList.js और Chat.js में action भेजने की आवश्यकता है।

import { useReducer } from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';
import { initialState, messengerReducer } from './messengerReducer';

export default function Messenger() {
  const [state, dispatch] = useReducer(messengerReducer, initialState);
  const message = state.message;
  const contact = contacts.find((c) => c.id === state.selectedId);
  return (
    <div>
      <ContactList
        contacts={contacts}
        selectedId={state.selectedId}
        dispatch={dispatch}
      />
      <Chat
        key={contact.id}
        message={message}
        contact={contact}
        dispatch={dispatch}
      />
    </div>
  );
}

const contacts = [
  {id: 0, name: 'Taylor', email: 'taylor@mail.com'},
  {id: 1, name: 'Alice', email: 'alice@mail.com'},
  {id: 2, name: 'Bob', email: 'bob@mail.com'},
];