Introduction
গত লেকচারে আমরা যে প্রজেক্টটি করেছিলাম আজ সেটাকে আমরা রিফ্যাক্টর করবো। অর্থাৎ গত ক্লাসে আমরা সমস্ত কোড একটা ফাইলের মধ্যে লিখেছিলাম। আজ আমরা ভিন্ন ভিন্ন কম্পোনেন্ট বানিয়ে কিভাবে রিইউজ করতে পারি সেটা দেখবো।
Breakdown the app into components
আমাদের আগে দেখতে হবে কি কি কম্পোনেন্ট রিইউজেবল হতে পারে। আমরা একটু আগে আমাদের UI এর দিকে তাকাই।
আমরা দেখতে পাচ্ছি প্রথমে আমাদের দুইটা ইনপুট ফিল্ড আছে একইরকম। তাহলে ইনপুট ফিল্ড রিইউজ হতে পারে। এরপর আছে বাটন। প্রতিটি বাটন দেখতে একই রকম। সুতরাং এগুলো আমরা রিইউজ করতে পারি। এরপর হিস্টোরি আইটেমগুলো রিইউজ হতে পারে। আবার যে বাটন তৈরি করেছিলাম সেটা রিস্টোর বাটন হিসেবেও রিইউজ করতে পারি আমরা।
এখন সবচেয়ে বড় ইস্যু হলো ডাটা। এখন আমাদের সমস্ত ডাটা আছে অ্যাপ কম্পোনেন্টের মধ্যে। এখন কোন ডাটাগুলোকে আমাদের অ্যাপের মধ্যে রাখতে হবে এবং কোনগুলোকে বের করে ফেলতে হবে সেটা বুঝতে হবে আমাদের। যেমন ইনপুট স্টেট আমাদের অ্যাপ কম্পোনেন্টের মধ্যে লাগবে। এরপর রেজাল্ট যেহেতু আমরা শো করবো তাই রেজাল্টের স্টেটটাও দরকার হবে অ্যাপের মধ্যে। এরপর আছে হিস্টোরি। যেহেতু হিস্টোরি তৈরি হবে অপারেশনের মাধ্যমে, শো হবে হিস্টোরিতে, আবার যখন রিস্টোর করবো তখন ইনপুটে চলে আসবে তাই এই স্টেটকেও আমাদের অ্যাপের মধ্যে রাখতে হবে। এরপর আছে রিস্টোরড হিস্টোরি স্টেট। যেটা সবচেয়ে পেইনফুল একটা বিষয়। সেটা নিয়ে আমরা পরে কাজ করবো।
আমরা এবার স্ট্রাকচারটা বুঝার চেষ্টা করি।
প্রথমে ইনপুট থেকে ডাটা ক্রিয়েট হবে এবং তা স্টোর হবে অ্যাপে। এরপর অপারেশন সেই ডাটাকে কনজিউম করবে এবং নতুন ডাটা ক্রিয়েট করবে। আর হিস্টোরি অপারেশন থেকে প্রাপ্ত ডাটা কনজিউম করবে।
এবার এখান থেকে আমরা আমাদের কম্পোনেন্ট ক্রিয়েট করবো। আমরা src ফোল্ডারের মধ্যে components নামে একটা ডিরেক্টরি ক্রিয়েট করবো। এই ডিরেক্টরির মধ্যে আমরা ui, inputs, operations এবং history নামে চারটা ডিরেক্টরি ক্রিয়েট করবো। এবার এক এক করে আমাদের কম্পোনেন্টগুলো আমরা বানিয়ে নিবো।
Working with ui components
NumberField Component
আমরা প্রথমে আমাদের সবচেয়ে ছোট ui নিয়ে কাজ করি। প্রথমে আমরা এর মধ্যে NumberField.jsx নামে একটা ফাইল ক্রিয়েট করবো। এরপর এখানে আমরা নিচের কোডটা লিখবো।
const NumberField = ({ value, onChange, name }) => {
const style = {
padding: '0.25rem',
borderRadius: '0.1rem',
border: '1px solid gray',
background: '#fff',
outline: 'none',
};
return (
<input
style={style}
type="number"
value={value}
onChange={onChange}
name={name}
/>
);
};
export default NumberField;
এবার এই কম্পোনেন্টকে আমরা App.jsx এ গিয়ে ইমপোর্ট করে ইউজ করবো।
import NumberField from './components/ui/NumberField';
const App = () => {
return (
<div style={{ width: '50%', margin: '0 auto' }}>
<h1>Result: {result}</h1>
<div>
<p>Inputs</p>
<NumberField
value={inputState.a}
onChange={handleInputChange}
name="a"
/>
<NumberField
value={inputState.b}
onChange={handleInputChange}
name="b"
/>
</div>
</div>
);
};
এখন এখানে যদি আমরা প্রপ্স নাও দিই তাও এটা শো করবে। কিন্তু আমরা তা চাই না। আমরা চাই যদি প্রপ্স না দিই তাহলে তা আমাদেরকে এরর বা ওয়ার্নিং দিবে। টাইপস্ক্রিপ্টে প্রপ টাইপ্স মেনশন করার অপশন আছে। কিন্তু যেহেতু আমরা জাভাস্ক্রিপ্ট নিয়ে কাজ করছি তাই আমরা prop-types লাইব্রেরিটা ব্যবহার করবো এখানে। এটা ইনস্টল করে আমরা Numberfield.jsx এ এটা ইমপোর্ট করবো। সেই সাথে আমরা কিছু কোড লিখবো।
import PropTypes from 'prop-types';
const NumberField = ({ value, onChange, name }) => {
const style = {
padding: '0.25rem',
borderRadius: '0.1rem',
border: '1px solid gray',
background: '#fff',
outline: 'none',
};
return (
<input
style={style}
type="number"
value={value}
onChange={onChange}
name={name}
/>
);
};
NumberField.protoTypes = {
value: PropTypes.number.isRequired,
onChange: PropTypes.func.isRequired,
name: PropTypes.string.isRequired,
};
export default NumberField;
এবার আমরা যদি প্রপ্স ছাড়া এমনি <NumberField />
এভাবে ব্যবহার করি দেখুন আমাদেরকে ওয়ার্নিং দিচ্ছে।
আমাদের একটা কম্পোনেন্ট রিইউজেবল করা হয়েছে।
Buttton Component
এবার আমরা আমাদের অ্যাপ্লিকেশনের বাটনগুলো রিইউজেবল করবো। তার জন্য src/components/ui এর মধ্যে Button.jsx নামে একটা ফাইল ক্রিয়েট করবো।
import PropTypes from 'prop-types';
const Button = ({ text, onClick, disabled, customStyle }) => {
const disabledStyle = {
backgroundColor: '#999',
color: '#333',
cursor: 'not-allowed',
};
const style = {
padding: '0.25rem 1rem',
backgroundColor: '#ddd',
color: '#212121',
borderRadius: '0.10rem',
cursor: 'pointer',
border: 'none',
...customStyle,
...(disabled && disabledStyle),
};
return (
<button style={style} onClick={onClick} disabled={disabled}>
{text}
</button>
);
};
Button.propTypes = {
onClick: PropTypes.object.isRequired,
text: PropTypes.string.isRequired,
disabled: PropTypes.bool,
customStyle: PropTypes.object,
};
Button.defaultProps = {
customStyle: {},
disabled: false,
};
export default Button;
এখানে আমরা প্রপ টাইপস ডিফাইন করে দিয়েছি। সেই সাথে customStyle এবং disabled এর ডিফল্ট ভ্যালু কি হবে সেটাও ডিফাইন করে দিলাম।
আমাদের বাটন কম্পোনেন্টও বানানো শেষ। এবং একই সাথে ui নিয়ে কাজও শেষ আমাদের। এবার আমরা ইনপুট কম্পোনেন্ট নিয়ে কাজ করবো।
Working with inputs components
আমরা src/components/inputs এর মধ্যে InputSection.jsx নামে একটা ফাইল ক্রিয়েট করে নিলাম।
import PropTypes from 'prop-types';
import NumberField from '../ui/NumberField';
const InputSection = ({ inputs, handleInputChange }) => {
return (
<div
style={{
width: '100%',
padding: '0.5rem 1rem',
backgroundColor: '#efefef',
borderRadius: '0.10rem',
}}
>
<h3
style={{
fontFamily: 'Arial',
textAlign: 'center',
fontSize: '1.5rem',
color: '#212121',
margin: 0,
marginBottom: '1rem',
}}
>
Inputs
</h3>
<div
style={{
display: 'flex',
gap: '1rem',
justifyContent: 'space-between',
}}
>
<NumberField value={inputs.a} onChange={handleInputChange} name="a" />
<NumberField value={inputs.b} onChange={handleInputChange} name="b" />
</div>
</div>
);
};
InputSection.propTypes = {
inputs: PropTypes.shape({
a: PropTypes.number.isRequired,
b: PropTypes.number.isRequired,
}).isRequired,
handleInputChange: PropTypes.func.isRequired,
};
export default InputSection;
এবার এটাকে আমরা App.jsx এ ইউজ করবো।
import InputSection from './components/inputs/InputSection';
const App = () => {
return (
<div style={{ width: '50%', margin: '0 auto' }}>
<h1>Result: {result}</h1>
<InputSection inputs={inputState} handleInputChange={handleInputChange} />
</div>
);
};
আমাদের UI দাঁড়াবে ঠিক এরকম।
আমাদের ইনপুট নিয়ে কাজ শেষ। এবার আমরা অপারেশন নিয়ে কাজ করবো।
Working with operations components
আমরা প্রতিবারের মতো src/components/operations এর মধ্যে OperationSection.jsx নামে একটা ফাইল ক্রিয়েট করে নিই। এখানে আমাদের দরকার একটা আইডি। সেজন্য আমরা shortid প্যাকেজটা ইনস্টল করে নিবো।
import PropTypes from 'prop-types';
import shortid from 'shortid';
import Button from '../ui/Button';
const OperationSection = ({ handleArithmeticOps, handleClearOps }) => {
const operations = [
{
id: shortid.generate(),
text: '+',
onClick: () => handleArithmeticOps('+'),
},
{
id: shortid.generate(),
text: '-',
onClick: () => handleArithmeticOps('-'),
},
{
id: shortid.generate(),
text: '*',
onClick: () => handleArithmeticOps('*'),
},
{
id: shortid.generate(),
text: '/',
onClick: () => handleArithmeticOps('/'),
},
{
id: shortid.generate(),
text: '%',
onClick: () => handleArithmeticOps('%'),
},
{
id: shortid.generate(),
text: '**',
onClick: () => handleArithmeticOps('**'),
},
{
id: shortid.generate(),
text: 'Clear',
onClick: handleClearOps,
},
];
return (
<div>
<p>Operations</p>
<div style={{ display: 'flex', gap: '0.5rem' }}>
{operations.map((operation) => (
<Button
text={operation.text}
onClick={operation.onClick}
key={operation.id}
/>
))}
</div>
</div>
);
};
OperationSection.propTypes = {
handleArithmeticOps: PropTypes.func.isRequired,
handleClearOps: PropTypes.func.isRequired,
};
export default OperationSection;
যেহেতু সব একই টাইপের বাটন তাই আমরা বাটনের ডাটাগুলোর অবজেক্টের একটা অ্যারে নিয়ে নিলাম। এরপর সেটা ম্যাপ করে দিলাম। এরপর আমরা এটাকে App.jsx এ ইমপোর্ট করে ইউজ করবো।
import InputSection from './components/inputs/InputSection';
import OperationSection from './components/operations/OperationSection';
const App = () => {
return (
<div style={{ width: '50%', margin: '0 auto' }}>
<h1>Result: {result}</h1>
<InputSection inputs={inputState} handleInputChange={handleInputChange} />
<OperationSection
handleArithmeticOps={handleArithmeticOps}
handleClearOps={handleClearOps}
/>
</div>
);
};
সেই সাথে আমাদের অপারেশন কম্পোনেন্ট নিয়ে কাজও শেষ হলো। আমাদের বাকি আছে হিস্টোরি কম্পোনেন্ট। আমরা এখন সেটা নিয়ে কাজ করবো।
Working with history component
এখানে আমাদের দুইটা কাজ আছে - History এবং HistoryItem। আমরা প্রথমে HistoryItem নিয়ে কাজ করবো। তার জন্য আমরা src/components/history এর মধ্যে HistoryItem.jsx নামে একটা ফাইল ক্রিয়েট করবো।
import PropTypes from 'prop-types';
import { useState } from 'react';
import Button from '../ui/Button';
const HistoryItem = ({ historyItem, disabled, handleRestoreBtn }) => {
const [show, setShow] = useState(false);
const handleToggle = () => {
setShow(!show);
};
return (
<li key={historyItem.id}>
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<p>
Operation: {historyItem.inputs.a} {historyItem.operation}{' '}
{historyItem.inputs.b}, Result: {historyItem.result}
</p>
<div>
<Button text={show ? 'Hide' : 'Show'} onClick={handleToggle} />
</div>
</div>
{show && (
<>
<small>
{historyItem.date.toLocaleDateString()}{' '}
{historyItem.date.toLocaleTimeString()}
</small>
<br />
<Button
text="Restore"
onClick={() => handleRestoreBtn(historyItem)}
disabled={disabled}
/>
</>
)}
</li>
);
};
HistoryItem.propTypes = {
historyItem: PropTypes.shape({
id: PropTypes.number.isRequired,
inputs: PropTypes.shape({
a: PropTypes.number.isRequired,
b: PropTypes.number.isRequired,
}).isRequired,
operation: PropTypes.string.isRequired,
result: PropTypes.number.isRequired,
date: PropTypes.object.isRequired,
}),
disabled: PropTypes.bool.isRequired,
handleRestoreBtn: PropTypes.func.isRequired,
};
HistoryItem.defaultProps = {
disabled: false,
};
export default HistoryItem;
এবার আমরা src/components/history এ HistorySection.jsx নামে একটা ফাইল ক্রিয়েট করবো।
import PropTypes from 'prop-types';
import HistoryItem from './HistoryItem';
const HistorySection = ({ histories, restoredHistory, handleRestoreBtn }) => {
return (
<div>
<p>History</p>
{histories.length === 0 ? (
<p>
<small>There is no history</small>
</p>
) : (
<ul>
{histories.map((historyItem) => (
<HistoryItem
key={historyItem.id}
disabled={restoredHistory === historyItem.id}
historyItem={historyItem}
handleRestoreBtn={handleRestoreBtn}
/>
))}
</ul>
)}
</div>
);
};
HistoryItem.propTypes = {
histories: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number.isRequired,
inputs: PropTypes.shape({
a: PropTypes.number.isRequired,
b: PropTypes.number.isRequired,
}).isRequired,
operation: PropTypes.string.isRequired,
result: PropTypes.number.isRequired,
date: PropTypes.object.isRequired,
})
),
restoredHistory: PropTypes.number.isRequired,
handleRestoreBtn: PropTypes.func.isRequired,
};
export default HistorySection;
এবার আমরা HistorySection কে App.jsx এ import করে দিবো।
import HistorySection from './components/history/HistorySection';
import InputSection from './components/inputs/InputSection';
import OperationSection from './components/operations/OperationSection';
const App = () => {
return (
<div style={{ width: '50%', margin: '0 auto' }}>
<h1>Result: {result}</h1>
<InputSection inputs={inputState} handleInputChange={handleInputChange} />
<OperationSection
handleArithmeticOps={handleArithmeticOps}
handleClearOps={handleClearOps}
/>
<HistorySection
histories={histories}
handleRestoreBtn={handleRestoreBtn}
restoredHistory={restoredHistory}
/>
</div>
);
};
Our Final UI
আমাদের ফাইনাল UI দেখতে হবে এরকম -
Source Code
এই লেকচারের সমস্ত সোর্স কোড আপনারা এই লিংক এ পাবেন।