State Logic-ஐ Reducer-க்குள் பிரித்தெடுத்தல்
பல event handler-களில் பரவியிருக்கும் பல state update-களை கொண்ட component-கள் சிக்கலாக மாறலாம். இத்தகைய சூழல்களில், state update logic அனைத்தையும் உங்கள் component-க்கு வெளியே உள்ள ஒரே function-இல் ஒன்றிணைக்கலாம்; அதற்குப் பெயர் reducer.
நீங்கள் கற்றுக்கொள்ள போவது
- reducer function என்றால் என்ன
useState-ஐuseReducerஆக எப்படி refactor செய்வது- reducer-ஐ எப்போது பயன்படுத்துவது
- ஒன்றை நன்றாக எப்படி எழுதுவது
reducer மூலம் state logic-ஐ ஒன்றிணைக்கவும்
உங்கள் component-கள் சிக்கலாக வளரும்போது, ஒரு component-ன் state எந்தெந்த வழிகளில் update ஆகிறது என்பதை ஒரு பார்வையில் புரிந்துகொள்வது கடினமாகலாம். உதாரணமாக, கீழே உள்ள TaskApp component state-இல் tasks array-ஐ வைத்திருக்கிறது, மேலும் task-களை சேர்க்க, நீக்க, திருத்த மூன்று வேறு event handler-களைப் பயன்படுத்துகிறது:
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 பயணத் திட்டம்</h1> <AddTask onAddTask={handleAddTask} /> <TaskList tasks={tasks} onChangeTask={handleChangeTask} onDeleteTask={handleDeleteTask} /> </> ); } let nextId = 3; const initialTasks = [ {id: 0, text: 'Kafka Museum-ஐ பார்வையிடு', done: true}, {id: 1, text: 'பொம்மலாட்டம் பாருங்கள்', done: false}, {id: 2, text: 'Lennon Wall படம்', done: false}, ];
state-ஐ update செய்ய அதன் ஒவ்வொரு event handler-மும் setTasks-ஐ call செய்கிறது. இந்த component வளரும்போது, அதற்குள் சிதறிக் கிடக்கும் state logic-ன் அளவும் அதிகரிக்கும். இந்த சிக்கலைக் குறைத்து, உங்கள் logic அனைத்தையும் நேரடியாக அணுகக்கூடிய ஒரே இடத்தில் வைத்திருக்க, அந்த state logic-ஐ உங்கள் component-க்கு வெளியே உள்ள ஒரே function-க்குள் நகர்த்தலாம்; அதற்கு “reducer” என்று பெயர்.
Reducer-கள் state-ஐ கையாளும் வேறு வழி. useState-இலிருந்து useReducer-க்கு மூன்று படிகளில் migrate செய்யலாம்:
- state அமைப்பதிலிருந்து action-களை dispatch செய்வதற்கு நகரவும்.
- reducer function ஒன்றை எழுதவும்.
- உங்கள் component-இலிருந்து reducer-ஐ பயன்படுத்தவும்.
படி 1: state அமைப்பதிலிருந்து action-களை dispatch செய்வதற்கு நகரவும்
உங்கள் event handler-கள் தற்போது 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));
}state அமைக்கும் logic அனைத்தையும் நீக்குங்கள். மீதமிருப்பது மூன்று event handler-கள்:
- பயனர் “சேர்” அழுத்தும்போது
handleAddTask(text)call செய்யப்படுகிறது. - பயனர் task-ஐ toggle செய்யும்போது அல்லது “சேமி” அழுத்தும்போது
handleChangeTask(task)call செய்யப்படுகிறது. - பயனர் “நீக்கு” அழுத்தும்போது
handleDeleteTask(taskId)call செய்யப்படுகிறது.
Reducer-களுடன் state-ஐ நிர்வகிப்பது, state-ஐ நேரடியாக அமைப்பதிலிருந்து சற்று வேறுபடும். state அமைத்து React-க்கு “என்ன செய்ய வேண்டும்” என்று சொல்லுவதற்கு பதிலாக, உங்கள் event handler-களிலிருந்து “action”-களை dispatch செய்வதன் மூலம் “பயனர் இப்போது என்ன செய்தார்” என்பதை குறிப்பிடுகிறீர்கள். (state update logic வேறு இடத்தில் இருக்கும்!) அதனால் event handler வழியாக “tasks-ஐ அமைப்பது” அல்ல, “ஒரு task சேர்க்கப்பட்டது/மாற்றப்பட்டது/நீக்கப்பட்டது” என்ற action-ஐ dispatch செய்கிறீர்கள். இது பயனரின் நோக்கத்தை அதிகமாக விளக்குகிறது.
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-க்கு pass செய்யும் object “action” என்று அழைக்கப்படுகிறது:
function handleDeleteTask(taskId) {
dispatch(
// "action" object:
{
type: 'deleted',
id: taskId,
}
);
}இது ஒரு சாதாரண JavaScript object. அதில் என்ன வைக்க வேண்டும் என்பதை நீங்கள் தீர்மானிக்கிறீர்கள்; ஆனால் பொதுவாக அது என்ன நடந்தது என்பதற்கான குறைந்தபட்ச தகவலை கொண்டிருக்க வேண்டும். (dispatch function-ஐ பின்னர் ஒரு படியில் சேர்ப்பீர்கள்.)
படி 2: reducer function ஒன்றை எழுதுங்கள்
reducer function என்பது உங்கள் state logic-ஐ வைக்கும் இடம். இது இரண்டு argument-களை எடுக்கிறது: தற்போதைய state மற்றும் action object. பின்னர் அடுத்த state-ஐ return செய்கிறது:
function yourReducer(state, action) {
// return next state for React to set
}reducer-இலிருந்து நீங்கள் return செய்வதையே React state ஆக அமைக்கும்.
இந்த உதாரணத்தில் உங்கள் event handler-களிலிருந்து state அமைக்கும் logic-ஐ reducer function-க்கு நகர்த்த, நீங்கள்:
- தற்போதைய state-ஐ (
tasks) முதல் argument ஆக அறிவிக்க வேண்டும். actionobject-ஐ இரண்டாவது argument ஆக அறிவிக்க வேண்டும்.- reducer-இலிருந்து அடுத்த state-ஐ return செய்ய வேண்டும் (React இதையே state ஆக அமைக்கும்).
state அமைக்கும் logic முழுவதும் reducer function-க்கு migrate செய்யப்பட்ட வடிவம் இதோ:
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('தெரியாத action: ' + action.type);
}
}reducer function state-ஐ (tasks) argument ஆக எடுப்பதால், அதை உங்கள் component-க்கு வெளியே அறிவிக்கலாம். இது indentation அளவைக் குறைத்து, உங்கள் code-ஐ வாசிக்க உதவும்.
Deep Dive
reducer-கள் உங்கள் component-க்குள் இருக்கும் code அளவை “reduce” செய்ய முடிந்தாலும், உண்மையில் அவை array-களில் செய்யக்கூடிய reduce() operation-ன் பெயரிலிருந்து வந்தவை.
reduce() operation ஒரு array-ஐ எடுத்து, பல மதிப்புகளிலிருந்து ஒரே மதிப்பை “accumulate” செய்ய அனுமதிக்கிறது:
const arr = [1, 2, 3, 4, 5];
const sum = arr.reduce(
(result, number) => result + number
); // 1 + 2 + 3 + 4 + 5நீங்கள் reduce-க்கு pass செய்யும் function “reducer” என்று அழைக்கப்படுகிறது. அது இதுவரை உள்ள result மற்றும் தற்போதைய item-ஐ எடுத்து, அடுத்த result-ஐ return செய்கிறது. React reducer-களும் இதே எண்ணத்தின் உதாரணம்: அவை இதுவரை உள்ள state மற்றும் action-ஐ எடுத்து, அடுத்த state-ஐ return செய்கின்றன. இவ்வாறு, காலப்போக்கில் action-களை state ஆகச் சேர்த்துக்கொள்கின்றன.
உங்கள் reducer function-ஐ pass செய்து, initialState மற்றும் actions array உடன் reduce() method-ஐப் பயன்படுத்தி final state-ஐ கணக்கிடவும் முடியும்:
import tasksReducer from './tasksReducer.js'; let initialState = []; let actions = [ {type: 'added', id: 1, text: 'Kafka Museum-ஐ பார்வையிடு'}, {type: 'added', id: 2, text: 'பொம்மலாட்டம் பாருங்கள்'}, {type: 'deleted', id: 1}, {type: 'added', id: 3, text: 'Lennon Wall படம்'}, ]; let finalState = actions.reduce(tasksReducer, initialState); const output = document.getElementById('output'); output.textContent = JSON.stringify(finalState, null, 2);
இதை நீங்கள் நீங்களே செய்ய வேண்டியிருப்பதில்லை, ஆனால் React செய்வதற்கு இது ஒத்ததாகும்!
படி 3: உங்கள் component-இலிருந்து reducer-ஐப் பயன்படுத்துங்கள்
இறுதியாக, tasksReducer-ஐ உங்கள் component-க்கு hook up செய்ய வேண்டும். React-இலிருந்து useReducer Hook-ஐ import செய்யுங்கள்:
import { useReducer } from 'react';பிறகு useState-ஐ மாற்றலாம்:
const [tasks, setTasks] = useState(initialTasks);இவ்வாறு useReducer-ஆல்:
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);useReducer Hook useState-க்கு ஒத்தது; அதற்கு initial state-ஐ pass செய்ய வேண்டும், அது stateful value-ஐயும் state அமைக்கும் வழியையும் (இந்தச் சூழலில் dispatch function) return செய்கிறது. ஆனால் இது சற்று வேறுபடும்.
useReducer Hook இரண்டு argument-களை எடுக்கிறது:
- reducer function
- initial state
மேலும் இது return செய்கிறது:
- stateful value
- dispatch function (பயனர் action-களை reducer-க்கு “dispatch” செய்ய)
இப்போது இது முழுமையாக இணைக்கப்பட்டுள்ளது! இங்கே, reducer component file-ன் கீழே அறிவிக்கப்பட்டுள்ளது:
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 பயணத் திட்டம்</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('தெரியாத action: ' + action.type); } } } let nextId = 3; const initialTasks = [ {id: 0, text: 'Kafka Museum-ஐ பார்வையிடு', done: true}, {id: 1, text: 'பொம்மலாட்டம் பாருங்கள்', done: false}, {id: 2, text: 'Lennon Wall படம்', done: false}, ];
விரும்பினால், reducer-ஐ வேறு file-க்குக் கூட நகர்த்தலாம்:
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 பயணத் திட்டம்</h1> <AddTask onAddTask={handleAddTask} /> <TaskList tasks={tasks} onChangeTask={handleChangeTask} onDeleteTask={handleDeleteTask} /> </> ); } let nextId = 3; const initialTasks = [ {id: 0, text: 'Kafka Museum-ஐ பார்வையிடு', done: true}, {id: 1, text: 'பொம்மலாட்டம் பாருங்கள்', done: false}, {id: 2, text: 'Lennon Wall படம்', done: false}, ];
இவ்வாறு concern-களைப் பிரித்தால் component logic-ஐ வாசிப்பது நேரடியாக இருக்கும். இப்போது event handler-கள் action-களை dispatch செய்வதன் மூலம் என்ன நடந்தது என்பதை மட்டுமே குறிப்பிடுகின்றன; reducer function அவற்றுக்கு பதிலாக state எப்படி update ஆகிறது என்பதை தீர்மானிக்கிறது.
useState மற்றும் useReducer-ஐ ஒப்பிடுதல்
Reducer-களுக்கும் குறைகள் இல்லாமல் இல்லை! அவற்றை ஒப்பிட சில வழிகள் இதோ:
- Code அளவு: பொதுவாக
useState-இல் தொடக்கத்தில் குறைவான code எழுத வேண்டும்.useReducer-இல் reducer function-ஐயும் action-களை dispatch செய்வதையும் எழுத வேண்டும். ஆனால் பல event handler-கள் state-ஐ ஒரேபோன்ற முறையில் மாற்றினால், code-ஐ குறைக்கuseReducerஉதவும். - வாசிப்புத் தெளிவு: state update-கள் நேரடியானவை என்றால்
useState-ஐ வாசிப்பது சாத்தியம். அவை சிக்கலாகும்போது, உங்கள் component code பெரிதாகி scan செய்வது கடினமாகலாம். இந்த நிலையில், update logic-ன் எப்படி என்பதையும் event handler-களின் என்ன நடந்தது என்பதையும் சுத்தமாகப் பிரிக்கuseReducerஉதவும். - Debugging:
useStateஉடன் bug இருந்தால், state எங்கே தவறாக அமைக்கப்பட்டது, ஏன் என்று சொல்ல கடினமாகலாம்.useReducerஉடன், ஒவ்வொரு state update-யையும் அது ஏன் நடந்தது (எந்தactionகாரணமாக) என்பதையும் பார்க்க reducer-க்குள் console log சேர்க்கலாம். ஒவ்வொருaction-மும் சரியாக இருந்தால், பிழை reducer logic-இல்தான் உள்ளது என்று தெரியும். ஆனால்useState-ஐ விட அதிக code வழியாக step செய்ய வேண்டும். - Testing: reducer என்பது உங்கள் component-ஐ சாராத pure function. இதனால் அதை export செய்து தனியாக test செய்யலாம். பொதுவாக component-களை இன்னும் realistic சூழலில் test செய்வது சிறந்ததாயினும், சிக்கலான state update logic-க்கு, குறிப்பிட்ட initial state மற்றும் action-க்கு உங்கள் reducer குறிப்பிட்ட state-ஐ return செய்கிறது என்று assert செய்வது பயனுள்ளதாக இருக்கும்.
- தனிப்பட்ட விருப்பம்: சிலருக்கு reducer-கள் பிடிக்கும்; சிலருக்கு பிடிக்காது. அது சரி. இது விருப்பம் சார்ந்தது.
useStateமற்றும்useReducerஇடையே எப்போதும் முன்னும் பின்னும் convert செய்யலாம்: அவை equivalent!
ஒரு component-இல் தவறான state update-கள் காரணமாக அடிக்கடி bug-களைச் சந்தித்து, அதன் code-க்கு அதிக structure சேர்க்க விரும்பினால் reducer பயன்படுத்த பரிந்துரைக்கிறோம். எல்லாவற்றிற்கும் reducer பயன்படுத்த வேண்டியதில்லை: mix and match செய்யலாம்! ஒரே component-இல் useState மற்றும் useReducer இரண்டையும் பயன்படுத்தலாம்.
reducer-களை நன்றாக எழுதுதல்
reducer-களை எழுதும்போது இந்த இரண்டு குறிப்புகளை மனதில் வைத்திருங்கள்:
- Reducer-கள் pure ஆக இருக்க வேண்டும். state updater function-களைப் போலவே, reducer-களும் rendering நடக்கும் போது இயங்கும்! (Action-கள் அடுத்த render வரை queue செய்யப்படும்.) இதன் பொருள் reducer-கள் pure ஆக இருக்க வேண்டும்: ஒரே input-கள் எப்போதும் ஒரே output-ஐத் தர வேண்டும். அவை request-களை அனுப்பக்கூடாது, timeout-களை schedule செய்யக்கூடாது, அல்லது side effect-களை (component-க்கு வெளியே உள்ள விஷயங்களை பாதிக்கும் operation-கள்) செய்யக்கூடாது. அவை object-களையும் array-களையும் mutation இல்லாமல் update செய்ய வேண்டும்.
- ஒவ்வொரு action-மும் ஒரே ஒரு பயனர் interaction-ஐ விவரிக்க வேண்டும்; அது data-வில் பல மாற்றங்களுக்கு வழிவகுத்தாலும். உதாரணமாக, reducer நிர்வகிக்கும் ஐந்து field-கள் உள்ள form-இல் பயனர் “மீட்டமை” அழுத்தினால், ஐந்து தனித்தனி
set_fieldaction-களை dispatch செய்வதைவிட ஒரேreset_formaction-ஐ dispatch செய்வது பொருத்தமானது. reducer-இல் ஒவ்வொரு action-ஐயும் log செய்தால், எந்த interaction அல்லது response எந்த வரிசையில் நடந்தது என்பதை மீண்டும் கட்டமைக்க அந்த log போதுமான தெளிவாக இருக்க வேண்டும். இது debugging-க்கு உதவும்!
Immer மூலம் சுருக்கமான reducer-களை எழுதுதல்
சாதாரண state-இல் object-களையும் array-களையும் update செய்வதைப் போலவே, reducer-களை சுருக்கமாக்க Immer library-யைப் பயன்படுத்தலாம். இங்கே, useImmerReducer push அல்லது arr[i] = assignment மூலம் state-ஐ mutate செய்ய அனுமதிக்கிறது:
{ "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": {} }
Reducer-கள் pure ஆக இருக்க வேண்டும், எனவே அவை state-ஐ mutate செய்யக்கூடாது. ஆனால் Immer உங்களுக்கு mutate செய்ய பாதுகாப்பான சிறப்பு draft object-ஐ வழங்குகிறது. உள்ளே, draft-இல் நீங்கள் செய்த மாற்றங்களுடன் உங்கள் state-ன் copy-ஐ Immer உருவாக்கும். அதனால் useImmerReducer நிர்வகிக்கும் reducer-கள் தங்கள் முதல் argument-ஐ mutate செய்ய முடியும், state-ஐ return செய்ய வேண்டியதில்லை.
Recap
useState-இலிருந்துuseReducer-க்கு மாற்ற:- event handler-களிலிருந்து action-களை dispatch செய்யுங்கள்.
- கொடுக்கப்பட்ட state மற்றும் action-க்கு அடுத்த state-ஐ return செய்யும் reducer function-ஐ எழுதுங்கள்.
useState-ஐuseReducer-ஆல் மாற்றுங்கள்.
- Reducer-கள் கொஞ்சம் அதிக code எழுதச் செய்கின்றன, ஆனால் debugging மற்றும் testing-க்கு உதவுகின்றன.
- Reducer-கள் pure ஆக இருக்க வேண்டும்.
- ஒவ்வொரு action-மும் ஒரே ஒரு பயனர் interaction-ஐ விவரிக்க வேண்டும்.
- mutating style-இல் reducer-களை எழுத விரும்பினால் Immer-ஐப் பயன்படுத்துங்கள்.
Challenge 1 of 4: event handler-களிலிருந்து action-களை dispatch செய்யுங்கள்
தற்போது, ContactList.js மற்றும் Chat.js-இல் உள்ள event handler-களில் // TODO comment-கள் உள்ளன. அதனால் input-இல் type செய்வது வேலை செய்யவில்லை; button-களை click செய்தாலும் தேர்ந்தெடுக்கப்பட்ட recipient மாறவில்லை.
இந்த இரண்டு // TODO-களையும் தொடர்புடைய action-களை dispatch செய்யும் code-ஆல் மாற்றுங்கள். action-களின் எதிர்பார்க்கப்படும் shape மற்றும் type-ஐப் பார்க்க, messengerReducer.js-இல் உள்ள reducer-ஐச் சரிபாருங்கள். reducer ஏற்கனவே எழுதப்பட்டுள்ளது, எனவே அதை மாற்றத் தேவையில்லை. ContactList.js மற்றும் Chat.js-இல் action-களை dispatch செய்வதுதான் தேவையானது.
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'}, ];