بسیاری از توسعه دهندگان react که چند سال است با آن کار می کنند، دوست دارند با تایپ اسکریپت کار کنند اما با نحوه انجام آن آشنایی ندارند. ما می خواهیم در این مقاله به صورت خلاصه این موضوع را بررسی کنیم تا شروعی برای مطالعات شما باشد.
نکته: در نظر داشته باشید که این مقاله برای افراد تازه کار طراحی نشده است بلکه هدف اصلی آن ارائه پیشنهادات استاندارد برای تعیین تایپ در React و مثال هایی از آن است. برای مطالعه این مقاله آشنایی متوسط به بالا در تایپ اسکریپت و React الزامی است.
همه می دانند که آسان ترین راه شروع پروژه های React استفاده از پکیج create-react-app می باشد اما این پکیج برنامه را به صورت پیش فرض با فایل های JSX (جاوا اسکریپتی) شروع می کند. اگر بخواهید پروژه شما یک پروژه تایپ اسکریپتی باشد باید از فلگ template-- استفاده کرده و typescript را به آن پاس بدهید:
npx create-react-app roxo-tsr --template typescript
با اجرای دستور بالا پروژه شما شروع به نصب می کند و ساختاری دقیقا مشابه با react جاوا اسکریپتی دارد اما به جای فایل های JSX فایل های TSX را خواهید داشت. roxo-tsr نامی است که من برای پروژه خودم انتخاب کرده ام اما شما می توانید هر نام دیگری را انتخاب کنید.
پس از نصب پروژه، از پوشه src وارد فایل App.tsx شده و محتوای اضافی آن را حذف کنید تا محتوایش به شکل زیر در بیاید:
import React from 'react'; import './App.css'; function App() { return ( <div className="App"> </div> ); } export default App;
در مرحله بعدی بیایید یک کامپوننت بسیار ساده را بسازیم که عنوان صفحه را نمایش می دهد. برای ساخت این کامپوننت از توابع استفاده می کنیم:
import React from 'react'; import './App.css'; // My Component for ROXO-TSR function Heading({ title }: { title: string }) { return <h1>{title}</h1> } function App() { return ( <div className="App"> <Heading title="THE HEADING FOR ROXO-TSR!"></Heading> </div> ); } export default App;
همانطور که می بینید من کامپوننتی به نام Heading را تعریف کرده ام که پارامتری به نام title می گیرد. از آنجایی که prop ها در react به صورت شیء دریافت می شوند، من از destructuring استفاده کرده ام تا فقط title را دریافت کنیم. از طرفی برای تعیین تایپ آن از یک شیء استفاده کرده ایم که خصوصیت title را در قالب رشته (string) دارد. حالا در کامپوننت اصلی App از این کامپوننت Heading استفاده کرده ایم و رشته THE HEADING FOR ROXO-TSR را به عنوان prop پاس داده ایم. حالا اگر دستور npm start را اجرا کنید، برنامه در مرورگر برایتان باز می شود (آدرس http://localhost:3000) و heading خود را مشاهده می کنید.
شاید بپرسید چرا برای تعیین تایپ به شکل زیر عمل نکرده ایم:
function Heading({ title }: string) { return <h1>{title}</h1> }
شما باید به عنوان توسعه دهنده react بدانید که prop ها به صورت یک شیء دریافت می شوند بنابراین کد بالا به شما خطای به شکل زیر می دهد:
Type '{ title: string; }' is not assignable to type 'string'
تایپ ما در هنگام صدا زدن Heading و پاس دادن title یک شیء با خصوصیت title خواهد بود که طبیعتا با تایپ رشته (string) یکی نیست. یادتان باشد که مزیت اصلی تایپ اسکرپیت همین بررسی تایپ ها است. مثلا اگر حالا به title یک عدد را پاس بدهید با خطا روبرو می شوید چرا که تایپ مورد نظر شیء ای با خصوصیتی به نام title بود که باید رشته باشد نه یک عدد! بدین صورت می توانیم از خطاهای بسیار زیادی دوری کنیم.
حالا سوالی دیگر را داریم، اگر اصلا برای title تایپ خاصی را در نظر نگیریم چطور؟
function Heading({ title }: string) { return <h1>{title}</h1> }
این کد به ما خطای زیر را می دهد:
Binding element 'title' implicitly has an 'any' type.
به عبارتی title به صورت پیش فرض تایپ any را می گیرد که در تایپ اسکریپت باعث خطا می شود.
در ضمن در صورتی که طرفدار destructuring نیستید، می توانید از روش عادی پاس دادن prop ها نیز استفاده کنید:
import React from 'react'; import './App.css'; // My Component for ROXO-TSR function Heading(prop: { title: string }) { return <h1>{prop.title}</h1> } function App() { return ( <div className="App"> <Heading title="THE HEADING FOR ROXO-TSR!"></Heading> </div> ); } export default App;
در هر صورت کدهای شما کار می کنند و این موضوع نهایتا به خودتان بستگی دارد.
یکی دیگر از روش های پاس دادن داده به کامپوننت ها استفاده از children prop ها است. children prop ها نوع خاصی از prop ها هستند که به جای استفاده از حالت عادی prop ها (قرارگیری به صورت یک attribute روی کامپوننت - مثل title در کد بالا که به عنوان attribute ای برای Heading است) داخل تگ های آغازین و پایانی کامپوننت قرار می گیرند. مثال:
import React, { ReactElement, ReactNode } from 'react'; import './App.css'; // My Component for ROXO-TSR function Heading({ title }: { title: string }) { return <h1>{title}</h1> } function HeadingWithContent({ children }: { children: ReactNode }): ReactElement { return <h1>{children}</h1> } function App() { return ( <div className="App"> <Heading title="THE HEADING FOR ROXO-TSR!"></Heading> <HeadingWithContent>This is the content inside our function</HeadingWithContent> </div> ); } export default App;
در اینجا کامپوننت دیگری به نام HeadingWithContent را ساخته ایم اما این بار تایپ ها را کمی پیشرفته تر نوشته ایم. در ابتدا children را destructure کرده ایم (همانطور که می دانید props.children از prop های خاص است که به محتوای بین تگ های آغازین و پایانی اشاره می کند) اما من تایپ آن را ReactNode گذاشته ام. چرا؟ ReactNode یعنی یک node از JSX یا به زبان ساده تر یک عنصر React. به کد زیر توجه کنید:
function HeadingWithContent({ children }: { children: string }): ReactElement { return <h1>{children}</h1> } function App() { return ( <div className="App"> <Heading title="THE HEADING FOR ROXO-TSR!"></Heading> <HeadingWithContent>This is the content inside our function</HeadingWithContent> </div> ); }
در اینجا تایپ را روی string گذاشته ام و مشکلی هم نداریم چرا که محتوای بین تگ آغازین و پایانی این کامپوننت (رشته This is the content inside our function) یک رشته است اما اگر آن را به شکل زیر بنویسیم به خطا برخورد می کنیم:
function App() { return ( <div className="App"> <Heading title="THE HEADING FOR ROXO-TSR!"></Heading> <HeadingWithContent><strong>This is the content inside our function</strong></HeadingWithContent> </div> ); }
چرا؟ به دلیل اینکه از تگ <strong> استفاده کرده ایم که تایپ Element را دارد و طبیعتا Element با string یکی نیست. به همین دلیل برای جلوگیری از این مشکل از تایپ ReactNode استفاده کرده ایم. در ضمن اگر دوباره به کامپوننت نگاه کنید متوجه می شوید که کامپوننت ما تایپ ReactElement را برمی گرداند. ReactElement یعنی یک عنصر React را برمی گردانیم. معمولا نیازی به نوشتن صریح تایپ برگردانده شده نیست، من این کار را برای یادگیری شما انجام داده ام. React به صورت خودکار تایپ برگردانده شده را حدس می زند و در مثال بالا اگر خودمان تایپ ReactElement را تعیین نکرده باشیم، تایپ JSX.Element در نظر گرفته می شود.
در این بخش می خواهم تایپ ها را کمی پیشرفته تر کنم بنابراین بهتر است از default prop ها استفاده کنیم:
const defaultContainerProps = { heading: <strong>My Default Heading</strong> } function Container({ heading, children }: { children: ReactNode } & typeof defaultContainerProps) { return <div><h1>{heading}</h1>{children}</div>; } Container.defaultProps = defaultContainerProps; function App() { return ( <div className="App"> <Heading title="THE HEADING FOR ROXO-TSR!"></Heading> <HeadingWithContent><strong>This is the content inside our function</strong></HeadingWithContent> <Container>SOME DATA</Container> </div> ); }
همانطور که می بینید ابتدا شیء ای به نام defaultContainerProps تعریف می کنیم و یک خصوصیت به عنوان یک prop را در آن می گذاریم. حالا کامپوننتی به نام Container تعریف کرده ام که دو prop به نام های children و heading را می گیرد. برای تعیین تایپ این دو prop از یک تایپ ترکیبی یا کمپوزیت استفاده کرده ایم. برای این کار ابتدا تایپ children را روی ReactNode گذاشته ام و سپس از علامت & برای ترکیب یک تایپ دیگر با آن استفاده کرده ام. تایپ دیگر ما همان شیء defaultContainerProps می باشد. توجه کنید که تمام این بخش برای تعیین تایپ است.
از طرف دیگر خصوصیتی به نام defaultProps را داریم که روی کامپوننت ها تعریف می شود و مقدار پیش فرضی را برای prop هایشان در نظر می گیرد. من این مقدار پیش فرض را برابر با شیء defaultContainerProps قرار داده ام. حالا اگر به بخش استفاده از کامپوننت <Container> توجه کنید متوجه می شوید که رشته SOME DATA را به عنوان children به آن داده ایم اما خبری از پارامتر دیگر (heading) نیست. از آنجایی که خصوصیت defaultProps را برای کامپوننت Container تعریف کرده ایم کدهایتان نباید هیچ مشکلی داشته باشند و باید به راحتی اجرا شوند. از کجا بفهمیم که کدهایمان به شکل صحیح کار کرده اند؟ تیتر My Default Heading باید در مرورگر برایتان نمایش داده شود.
یک راه برای تمیزتر کردن کدهایتان این است که تایپ prop را جداگانه تعریف کنید:
const defaultContainerProps = { heading: <strong>My Default Heading</strong> } type ContainerPropType = { children: ReactNode } & typeof defaultContainerProps; function Container({ heading, children }: ContainerPropType) { return <div><h1>{heading}</h1>{children}</div>; }
انجام این کار باعث خواناتر شدن کدهایتان می شود اما به هر حال کاری سلیقه ای است.
کد زیر مجموعه ای از تایپ های مختلف را به شما نشان می دهد که در توسعه پروژه های React کاربردی هستند. من توضیحات اضافه را در مواردی که نیاز باشد در کنار آن بخش به صورت کامنت آورده ام:
type AppProps = { message: string; count: number; disabled: boolean; /** آرایه ای از یک تایپ خاص مانند رشته */ names: string[]; /** مشخص کردن تایپ بین یکی از دو رشته مشخص شده. در این حالت هیچ مقدار دیگری به جز یکی از این دو مجاز نیست. */ status: "waiting" | "success"; /** هر شیء ای می تواند باشد به شرطی که از خصوصیات آن استفاده نکنید */ obj: object; obj2: {}; // معادل تایپ قبلی است /** روش پیشنهاد شده و صحیح تر که خصوصیات شیء را مشخص می کند */ obj3: { id: string; title: string; }; /** آرایه ای از اشیاء */ objArr: { id: string; title: string; }[]; /** مشخص کردن تایپ کلید های یک شیء */ dict1: { [key: string]: MyTypeHere; }; dict2: Record<string, MyTypeHere>; // معادل مثال قبلی است /** هر تابعی می تواند باشد به شرطی که آن را صدا نزنید. استفاده از این تایپ پیشنهاد نمی شود */ onSomething: Function; /** تابعی که هیچ ورودی ندارد و چیزی نیز برنمی گرداند */ onClick: () => void; /** خاصی را با نام خاصی می گیرد prop تابعی که */ onChange: (id: number) => void; /** یا رویداد خاصی را می گیرد event تابعی که */ onClick(event: React.MouseEvent<HTMLButtonElement>): void; /** اختیاری و غیر اجباری prop */ optional?: OptionalType; };
حالا که با تایپ های کاربردی و ساده آشنا شدیم بهتر است نگاهی به تایپ های مخصوص prop ها نگاهی بیندازیم. این تایپ ها معمولا برای مشخص کردن تایپ prop هایی هستند که کامپوننت های دیگر را به عنوان prop دریافت می کنند:
export declare interface AppProps { children1: JSX.Element; // تایپ بدی است چرا که آرایه ها را شامل نمی شود children2: JSX.Element | JSX.Element[]; // تایپ متوسطی است اما رشته ها را شامل نمی شود children3: React.ReactChildren; // یک تایپ بد است که معمولا به عنوان تایپ کمکی از آن استفاده می شود children4: React.ReactChild[]; // تایپ بهتری است که از فرزندان آرایه ای نیز پشتیبانی می کند children: React.ReactNode; // بهترین تایپ است چرا که همه چیز را قبول می کند functionChildren: (name: string) => React.ReactNode; // recommended function as a child render prop type style?: React.CSSProperties; // هایی که مخصوص استایل دهی هستند prop برای onChange?: React.FormEventHandler<HTMLInputElement>; // برای رویدادهای فرم ها props: Props & React.ComponentPropsWithoutRef<"button">; // آن دکمه ref برای شبیه سازی تمام پراپ های یک دکمه بدون ارسال کردن props2: Props & React.ComponentPropsWithRef<MyButtonWithForwardRef>; // را نیز ارسال می کند ref دقیقا معادل مثال قبلی است با این تفاوت که }
تایپ خاصی در React وجود دارد که شهرت زیادی دارد و باید با آن آشنا باشید:
const HeadingFC: React.FC<{ title: string }> = ({ title }) => <h1>{title}</h1>
کدی که می بینید یک کامپوننت ساده React است که با Arrow Function ها نوشته شده است. ما با استفاده از تایپ React.FC تایپ برگردانده شده توسط این کامپوننت را به صورت صریح مشخص می کند. همچنین این کار باعث می شود prop پیش فرضی برای children در نظر گرفته شود. بسیاری از توسعه دهندگان React پیشنهاد می کنند از React.FC استفاده نکنید و درخواست هایی برای حذف آن نیز وجود دارد. در لینکی که برایتان قرار داده ام درباره معایب React.FC توضیح داده شده است؛ مثلا همانطور که گفتم استفاده از آن روی یک کامپوننت باعث می شود آن کامپوننت به صورت خودکار children بگیرد حتی اگر نخواهیم کامپوننت ما prop children داشته باشد:
const App: React.FC = () => { /*... */ }; const Example = () => { <App><div>Unwanted children</div></App> }
یکی دیگر از معایب آن این است که با استفاده از آن دیگر نمی توانید از generic ها استفاده کنید و بعضا در defaultProps نیز مشکل ایجاد می کند. این مسئله بحث هایی طولانی دارد اما به عنوان یک قانون کلی در نظر داشته باشید که بهتر است از تایپ React.FC یا حالت دیگر آن React.FunctionComponent استفاده نکنید.
برای تعریف تایپ prop های کامپوننت هایتان از type استفاده کنید، همچنین برای state نیز از type استفاده کنید چرا که محدود تر است اما برای دیگر موارد مانند API عمومی یا نوشتن یک کتابخانه از interface ها استفاده کنید چرا که کاربر می تواند تایپ اولیه شما را (که در قالب interface نوشته شده است) با تایپ مورد نظر خودش ادغام کرده و آن را گسترش دهد.
برای کامپوننت های عادی که یک آرگومان prop می گیرند و یک عنصر JSX برمی گردانند، تایپ های زیر از رایج ترین تایپ ها محسوب می شوند:
// prop مشخص کردن تایپ یک type AppProps = { message: string; }; /* اگر فرد دیگری بخواهد از آن استفاده کند، بهتر است از اینترفیس ها استفاده کنید تا آن فرد بتواند تایپ را گسترش بدهد */ // راحت ترین راه برای تعیین تایپ یک کامپوننت تابعی const App = ({ message }: AppProps) => <div>{message}</div>; // شما می توانید تایپ داده برگردانده شده توسط این کامپوننت را نیز مشخص کنید تا به صورت تصادفی و اشتباهی چیز اشتباهی را برنگردانید const App = ({ message }: AppProps): JSX.Element => <div>{message}</div>; // یک روش دیگر برای مشخص کردن تایپ است اما کمی کدها را شلوغ می کند. const App = ({ message }: { message: string }) => <div>{message}</div>;
شما می توانید از این تایپ ها برای prop های خودتان استفاده کنید.
از نسخه 16.8 کتابخانه react به بعد، تایپ های هوک ها (hook) در تایپ اسکریپت پشتیبانی می شوند بنابراین باید با آن ها آشنا شویم.
اولین هوک ما useState است:
const [val, toggle] = React.useState(false); // بولین باشد val تایپ اسکریپت حدس می زند که // نیز فقط مقادیر بولین را قبول می کند toggle
همانطور که در کامنت های کد بالا توضیح داده ام، در مثال های ساده ای مانند این مثال هیچ مشکلی در تشخیص تایپ نداریم و تایپ اسکریپت این کار را به صورت خودکار انجام می دهد اما در بسیاری از برنامه ها و هوک های دیگر مقدار را برابر null یا امثال آن می گذاریم. در چنین حالاتی می توانیم خودمان تایپ را به صورت صریح مشخص کنیم:
const [user, setUser] = React.useState<IUser | null>(null); // در ادامه کد setUser(newUser);
IUser نام یکی از اینترفیس های ما است بنابراین گفته ایم state ما یا از نوع IUser است و یا null خواهد بود. همچنین در برخی از موارد state در همان ابتدا مقدار می گیرد و تا انتهای برنامه مقدار خاصی خواهد داشت. در این حالت می توانید از type assertion استفاده کنید:
const [user, setUser] = React.useState<IUser>({} as IUser); // later... setUser(newUser);
با انجام این کار موقتا به کامپایلر تایپ اسکریپت دروغ گفته ایم که {} از تایپِ IUser است. در ادامه کدهایتان حتما باید state کاربر (user) را تعیین کنید، در غیر این صورت تمام کدهایتان فرض وابسته به این خواهند بود که user از نوع IUser است و این موضوع ممکن است باعث بروز خطا در هنگام runtime شود.
هوک بعدی ما useReducer است. برای مشخص کردن تاپی اکشن های useReducer می توانیم از تایپ های Discriminated Unions استفاده کنیم. در این حالت باید یادتان باشد که return type (تایپ برگردانده شده) را نیز مشخص کنید در غیر این صورت تایپ اسکریپت خودش آن را حدس می زند:
const initialState = { count: 0 }; type ACTIONTYPE = | { type: "increment"; payload: number } | { type: "decrement"; payload: string }; function reducer(state: typeof initialState, action: ACTIONTYPE) { switch (action.type) { case "increment": return { count: state.count + action.payload }; case "decrement": return { count: state.count - Number(action.payload) }; default: throw new Error(); } } function Counter() { const [state, dispatch] = React.useReducer(reducer, initialState); return ( <> Count: {state.count} <button onClick={() => dispatch({ type: "decrement", payload: "5" })}> - </button> <button onClick={() => dispatch({ type: "increment", payload: 5 })}> + </button> </> ); }
همانطور که می بینید ما تایپ خودمان را در قالب یک type نوشته ایم و به همین راحتی می توانیم از useReducer استفاده کنیم.
هوک بعدی useEffect است که نکته مهمی دارد. هنگام استفاده از این هوک حواستان باشد که به جز یک تابع یا undefined هیچ چیز دیگری را برنگردانید در غیر این صورت هم تایپ اسکریپت و هم react خطاهای مختلفی را پرتاب می کنند. مخصوصا اگر در حال استفاده از arrow function ها هستید باید بدانید که برگردانده مقادیر مختلف بسیار ساده است. مثال زیر یک مثال اشتباه استفاده از useEffect است:
function DelayedEffect(props: { timerMs: number }) { const { timerMs } = props; useEffect( () => setTimeout(() => { /* انجام عملیات مورد نظر */ }, timerMs), [timerMs] ); // در پس زمینه و به صورت ناخواسته یک عدد را برمی گرداند setTimeout این یک مثال بد است چرا که // چرا که بدنه تابع درون علامت های {} قرار نگرفته است return null; }
راه حل مشکل کد بالا بدین شکل است:
function DelayedEffect(props: { timerMs: number }) { const { timerMs } = props; useEffect(() => { setTimeout(() => { /* انجام عملیات مورد نظر */ }, timerMs); }, [timerMs]); // می توانید مطمئن شوید که چیزی را برنمی گردانید void با استفاده از return null; }
هوک بعدی ما useRef است. زمانی که در حال تعریف یک container برای ref هستید که هیچ مقدار اولیه ای ندارد، سه راه حل وجود دارد:
const ref1 = useRef<HTMLElement>(null!); const ref2 = useRef<HTMLElement>(null); const ref3 = useRef<HTMLElement | null>(null);
خط اول باعث می شود nullcheck ها روی ref1.current غیر فعال شوند و هدفش پاس دادن آن به یک خصوصیت ref است. در واقع علامت ! در کنار null به تایپ اسکریپت به دروغ می گوید که null از نظر تایپ null نیست!
راه دوم (خط دوم از کد بالا) به جای MutableRefObject از یک RefObject استفاده می کند بنابراین اگر سعی کنید به ref2.current مقدار دهی کنید با خطا روبرو می شوید. راه سوم نیز ref3.current را قابل تغییر (mutable) می کند و در حالاتی استفاده می شود که بخواهید خودتان نمونه های ساخته شده را مدیریت کنید:
function TextInputWithFocusButton() { // توضیحات بخش یک const inputEl = React.useRef<HTMLInputElement>(null); const onButtonClick = () => { // توضیحات بخش دو if (inputEl && inputEl.current) { inputEl.current.focus(); } }; return ( <> {/* توضیحات بخش سه */} <input ref={inputEl} type="text" /> <button onClick={onButtonClick}>Focus the input</button> </> ); }
من سه بخش مختلف را در کد بالا مشخص کرده ام و برای هر کدام توضیحاتی را خواهم داد:
استفاده از inline handler ها ساده ترین راه دریافت تایپ رویدادها به صورت خودکار است:
const el = ( <button onClick={(event) => { /* تایپ رویدادها به صورت خودکار توسط تایپ اسکریپت محاسبه می شود */ }} /> );
اما در بسیاری اوقات نمی توانیم از event handler های inline (مثل کد بالا) استفاده کنیم و مجبور هستیم handler هایمان را جداگانه تعریف کنیم. در چنین حالتی می توانید خودتان تایپ ها را انتخاب کنید اما سعی کنید از ادیتورهای مناسب مانند vscode استفاده کنید تا کارتان راحت تر شود:
type State = { text: string; }; class App extends React.Component<Props, State> { state = { text: "", }; // نوشتن تایپ ها در سمت راست علامت مساوی onChange = (e: React.FormEvent<HTMLInputElement>): void => { this.setState({ text: e.currentTarget.value }); }; render() { return ( <div> <input type="text" value={this.state.text} onChange={this.onChange} /> </div> ); } }
البته شما می توانید به جای استفاده از <>React.FormEvent و void تایپ هایتان را مستقیما به خود event handler بدهید:
// نوشتن تایپ ها در سمت چپ علامت مساوی onChange: React.ChangeEventHandler<HTMLInputElement> = (e) => { this.setState({text: e.currentTarget.value}) }
هر دو روش به شکل صحیح کار می کنند و استفاده از آن ها به سلیقه شما بستگی دارد.
اما در صورتی که نوع رویداد برایتان اهمیتی ندارد می توانید از React.SyntheticEvent استفاده نمایید که یک رویداد مصنوعی در react است. همچنین اگر فرم مورد نظرتان named input های خاصی دارد نیز می توانید از type assertion استفاده کنید:
<form ref={formRef} onSubmit={(e: React.SyntheticEvent) => { e.preventDefault(); const target = e.target as typeof e.target & { email: { value: string }; password: { value: string }; }; const email = target.email.value; // یا بررسی تایپ انجام می شود typechecks! const password = target.password.value; // یا بررسی تایپ انجام می شود typechecks! // etc... }} > <div> <label> Email: <input type="email" name="email" /> </label> </div> <div> <label> Password: <input type="password" name="password" /> </label> </div> <div> <input type="submit" value="Log in" /> </div> </form>
همانطور که می دانید context ها برای به اشتراک گذاری سراسری برخی prop ها در react استفاده دارند. تعیین تایپ برای آن ها در React کار آسانی است و پیچیدگی خاصی ندارد:
import * as React from "react"; interface AppContextInterface { name: string; author: string; url: string; } const AppCtx = React.createContext<AppContextInterface | null>(null); // در قسمتی از برنامه Provider استفاده از const sampleAppContext: AppContextInterface = { name: "Using React Context in a Typescript App", author: "thehappybug", url: "http://www.example.com", }; export const App = () => ( <AppCtx.Provider value={sampleAppContext}>...</AppCtx.Provider> ); // محل استفاده از آن در برنامه export const PostInfo = () => { const appContext = React.useContext(AppCtx); return ( <div> Name: {appContext.name}, Author: {appContext.author}, Url:{" "} {appContext.url} </div> ); };
من در ابتدا اینترفیس خودم را با نام AppContextInterface تعریف کرده ام. در مرحله بعدی مثل همیشه از createContext برای ساخت context استفاده می کنیم و سپس اینترفیس خودمان (AppContextInterface) را به عنوان تایپ به آن می دهیم اما توجه کنید که تایپ null را هم پاس بدهید چرا که در بسیاری از اوقات context در ابتدا null است (مانند مثال بالا). در مرحله بعد مقداری را برای یک context در نظر گرفته ایم و آن را در قالب شیء sampleAppContext قرار داده ایم، سپس با یک Provider از آن استفاده کرده ایم. در بخش انتهایی نیز می بینید که با هوک useContext از مقدار ذخیره شده در آن استفاده می کنیم.
در آخر باید یک ترفند جالب را به شما یاد بدهم. اگر می خواهید می توانید تایپ context را روی یک شیء خالی قرار بدهید:
interface ContextState { // ای که می خواهید با کانتکست مدیریت کنید را مشخص می کنید state در این بخش نوع name: string | null; } // را به عنوان یک شیء خالی در نظر می گیریم state حالا const Context = React.createContext({} as ContextState);
ref ها یکی دیگر از قابلیت های عالی react هستند اما در هنگام استفاده از آن ها ممکن است نتوانید تایپ صحیحشان را حدس بزنید. بالاتر در رابطه با useRef صحبت کردم اما حالا می خواهم در رابطه با createRef و forwardRef صحبت کنم. تعیین تایپ این دو مورد نیز آسان است؛ برای createRef به شکل زیر عمل می کنیم:
class CssThemeProvider extends React.PureComponent<Props> { private rootRef = React.createRef<HTMLDivElement>(); // مثالی ساده render() { return <div ref={this.rootRef}>{this.props.children}</div>; } }
همانطور که می بینید با استفاده از تایپ HTMLDivElement یک ref از نوع عنصر div ایجاد کرده ایم. همین مسئله برای forwardRef نیز صادق است و می توان گفت:
type Props = { children: React.ReactNode; type: "submit" | "button" }; export type Ref = HTMLButtonElement; export const FancyButton = React.forwardRef<Ref, Props>((props, ref) => ( <button ref={ref} className="MyClassName" type={props.type}> {props.children} </button> ));
از آنجایی که این کد بسیار ساده است، فکر نمی کنم نیازی به توضیح اضافه داشته باشد.
تعیین تایپ برای portal های react بسیار ساده است. برای انجام این کار نیازی به انجام عملیات پیشرفته ای ندارید:
const modalRoot = document.getElementById("modal-root") as HTMLElement; export class Modal extends React.Component { el: HTMLElement = document.createElement("div"); componentDidMount() { modalRoot.appendChild(this.el); } componentWillUnmount() { modalRoot.removeChild(this.el); } render() { return ReactDOM.createPortal(this.props.children, this.el); } }
فرض ما در کد بالا این است که در HTML خود عنصری از نوع div با آیدی modal-root دارید. ما در همان ابتدا تایپ آن را روی تایپ کلی HTMLElement تنظیم کرده ایم. همانطور که می بینید به غیر از این مورد نیازی به انجام کار دیگری نیست!
همانطور که می دانید Error boundary ها از قابلیت های جدید در نسخه 16 از کتابخانه react هستند. قبل از معرفی error boundary ها اگر درون کامپوننتی خطا داشتیم، کل State برنامه react شما بهم میریخت و پیام های خطای رمزگونه ای دریافت می کردید اما با error boundary ها این مشکل حل شده است. برای استفاده از Error boundary ها با تایپ اسکریپت ها راه های مختلفی وجود دارد اما بهترین راه حل که من به شما پیشنهاد می دهم استفاده از پکیجی به نام React-error-boundary است که به صورت پیش فرض از تایپ اسکرپیت استفاده کرده و به شما اجازه می دهد بدون هیچ مشکلی قابلیت error boundary را در پروژه های react خود پیاده کنید.
اگر دوست ندارید از این پکیج استفاده کنید، راه دیگر ساخت یک کامپوننت جداگانه برای استفاده از error boundary ها است. به مثال زیر توجه کنید:
import React, { Component, ErrorInfo, ReactNode } from "react"; interface Props { children: ReactNode; } interface State { hasError: boolean; } class ErrorBoundary extends Component<Props, State> { public state: State = { hasError: false }; public static getDerivedStateFromError(_: Error): State { // در این بخش وضعیت برنامه را به روز رسانی می کنیم تا به جای دریافت خطاهای عجیب و غریب پیام خطا را از رابط گرافیکی مشاهده کنیم. return { hasError: true }; } public componentDidCatch(error: Error, errorInfo: ErrorInfo) { console.error("Uncaught error:", error, errorInfo); } public render() { if (this.state.hasError) { return <h1>Sorry.. there was an error</h1>; } return this.props.children; } } export default ErrorBoundary;
با چنین کامپوننتی می توانیم از بهم ریختن کامل State برنامه خودمان دوری کنیم.
دنیای react و تایپ اسکریپت بسیار بزرگ تر از این چند مبحث ساده است اما این مقاله برای شروع کمک خوبی به شما است.
در این قسمت، به پرسشهای تخصصی شما دربارهی محتوای مقاله پاسخ داده نمیشود. سوالات خود را اینجا بپرسید.