تایپ اسکریپت یک زبان برنامه نویسی محبوب است، زیرا کارهای متعددی در کد ما انجام می‌دهد از جمله بررسی قوی تایپ استاتیک، قابل فهم بودن و استنتاج تایپ و غیره. هنگامی که تایپ اسکریپت همراه با React استفاده می‌شود، تجربه توسعه‌دهنده را بهبود می‌دهد و قابلیت پیش‌بینی بیشتری را برای پروژه ارائه می‌کند.

در این مقاله قصد داریم تا با انجام یک پروژه to-do list، با نحوه استفاده از تایپ اسکریپت با React Context آشنا شویم. برای استفاده حداکثری از این مقاله، داشتن آشنایی اولیه با React و تایپ اسکریپت لازم است. برای این منظور ما آموزش TypeScript – دوره جامع و آموزش React – دوره فشرده فرانت کست را پیشنهاد می‌کنیم.

منظور از React Context API چیست؟

React Context API در نسخه React 16 به عنوان راهی برای به اشتراک گذاشتن داده‌ها در یک درخت کامپوننت بدون نیاز به ارسال props در هر سطح معرفی شد.

Context API برای داده‌هایی که سراسری در نظر گرفته می‌شوند، ایده آل است. اما برای یک state manager اختصاصی مثل Redux یا MobX، برای کارهایی مانند زبان فعلی کاربر، تم فعلی یا حتی داده‌های یک فرم چند مرحله‌ای قبل از ارسال به یک API به اندازه کافی بزرگ یا پیچیده نیست.

نحوه راه‌اندازی برنامه

برای آشنایی با React Context، یک برنامه to-do list می‌سازیم که از Context API برای مدیریت تسک‌های موجود در لیست و همچنین برای موضوع theming استفاده می‌کند.

ما در این مقاله از Create React App برای داشتن یک پیکربندی مدرن و بدون مشکل استفاده می‌کنیم.

ترمینال را باز کرده و دستور زیر را در آن اجرا می‌کنیم:

npx create-react-app react-context-todo --template typescript

برای ساختن آسان یک پروژه تایپ اسکریپت با CRA، باید flag --template typescriptرا به پروژه اضافه کنیم، در غیر این صورت برنامه ما فقط از جاوااسکریپت پشتیبانی خواهد کرد.

در مرحله بعد، ساختار پروژه را به صورت زیر آماده می‌کنیم:

src
├── @types
│   └── todo.d.ts
├── App.tsx
├── components
│   ├── AddTodo.tsx
│   └── Todo.tsx
├── containers
│   └── Todos.tsx
├── context
│   └── todoContext.tsx
├── index.tsx
├── react-app-env.d.ts
└── styles.css

در اینجا، دو فایل داریم که باید به آن‌ها توجه داشته باشیم:

یکی از بهترین کارهایی که در پروژه خود می‌توانیم انجام دهیم این است که فایل‌های اختصاصی برای تعریف تایپ داشته باشیم. زیرا این کار ساختار پروژه ما را بهبود می‌بخشد. می‌توانیم تایپ‌هایی که تعریف کرده‌ایم را بدون import کردن و به صورت رفرنس استفاده نماییم. یا این که در ابتدا آن‌ها را export کرده و در فایل دیگری import کنیم و مورد استفاده قرار دهیم.

ساخت تایپ to-do

تایپ‌های تایپ اسکریپت این امکان را به ما می‌دهند تا بتوانیم تعیین کنیم که یک متغیر یا تابع، به عنوان مقداری که به کامپایلر کمک می‌کند تا قبل از زمان اجرا خطاها را تشخیص دهد، انتظار چه تایپی را داشته باشد:

// @types.todo.ts
export interface ITodo {
  id: number;
  title: string;
  description: string;
  status: boolean;
}
export type TodoContextType = {
  todos: ITodo[];
  saveTodo: (todo: ITodo) => void;
  updateTodo: (id: number) => void;
};

همانطور که می‌بینیم، اینترفیس ITodo شکل یک آبجکت to-do را تعریف می‌کند. در مرحله بعد، تایپ TodoContextType را داریم که آرایه‌ای از to-dos را export می‌کند و متدهایی برای افزودن یا به‌روزرسانی یک to-do انجام می‌دهد.

ساخت context

React Context ما را قادر می‌سازد تا بدون استفاده از props، بتوانیم state را در بین کامپوننت‌های خود به اشتراک بگذاریم و آن را مدیریت کنیم. context داده‌ها را فقط به کامپوننت‌هایی که به آن‌ها نیاز دارند ارائه می‌دهد:

// context/todoContext.tsx
import * as React from 'react';
import { TodoContextType, ITodo } from '../@types/todo';

export const TodoContext = React.createContext<TodoContextType | null>(null);

const TodoProvider: React.FC<{children: React.ReactNode}> = ({ children }) => {
  const [todos, setTodos] = React.useState<ITodo[]>([
    {
      id: 1,
      title: 'post 1',
      description: 'this is a description',
      status: false,
    },
    {
      id: 2,
      title: 'post 2',
      description: 'this is a description',
      status: true,
    },
  ]);

در کدی که داریم کار خود را با ایجاد یک context جدید و تنظیم تایپ آن برای مطابقت با TodoContextType یا null شروع می‌کنیم. هنگام ساخت context، مقدار پیش‌فرض را به طور موقت null می‌کنیم. مقادیر مورد نظر را به provider تخصیص می‌دهیم.

در مرحله بعد، کامپوننت TodoProvider را ایجاد می‌کنیم که context را برای مصرف‌کنندگان کامپوننت فراهم می‌کند. در اینجا، ما state را با برخی از داده‌ها مقداردهی اولیه می‌کنیم تا todos که داریم کار کند:

// context/todoContext.tsx
const saveTodo = (todo: ITodo) => {
  const newTodo: ITodo = {
    id: Math.random(), // not really unique - but fine for this example
    title: todo.title,
    description: todo.description,
    status: false,
  }
  setTodos([...todos, newTodo])
}

const updateTodo = (id: number) => {
  todos.filter((todo: ITodo) => {
    if (todo.id === id) {
      todo.status = true
      setTodos([...todos])
    }
  })
}

تابع saveTodo یک to-do جدید بر اساس اینترفیس ITodo ایجاد می‌کند و سپس آبجکت را به آرایه to-dos اضافه می‌نماید. تابع بعدی یعنیupdateTodo، به دنبال ID مربوط به to-doهای ارسال شده به عنوان پارامتر در آرایه to-dos می‌گردد و سپس آن را به‌روزرسانی می‌کند:

// context/todoContext.tsx
 return (
    <TodoContext.Provider value={{ todos, saveTodo, updateTodo }}>
      {children}
    </TodoContext.Provider>
  );
};

export default TodoProvider;

در مرحله بعد، مقادیر را به context ارسال می‌کنیم تا آن‌‌ها را برای کامپوننت‌ها قابل استفاده کنیم:

// context/todoContext.tsx
import React from 'react';
import { TodoContextType, ITodo } from '../@types/todo';

export const TodoContext = React.createContext<TodoContextType | null>(null);

const TodoProvider: React.FC<{children: React.ReactNode}> = ({ children }) => {
  const [todos, setTodos] = React.useState<ITodo[]>([
    {
      id: 1,
      title: 'post 1',
      description: 'this is a description',
      status: false,
    },
    {
      id: 2,
      title: 'post 2',
      description: 'this is a description',
      status: true,
    },
  ]);
  const saveTodo = (todo: ITodo) => {
    const newTodo: ITodo = {
      id: Math.random(), // not really unique - but fine for this example
      title: todo.title,
      description: todo.description,
      status: false,
    };
    setTodos([...todos, newTodo]);
  };
  const updateTodo = (id: number) => {
    todos.filter((todo: ITodo) => {
      if (todo.id === id) {
        todo.status = true;
        setTodos([...todos]);
      }
    });
  };
  return <TodoContext.Provider value={{ todos, saveTodo, updateTodo }}>{children}</TodoContext.Provider>;
};

export default TodoProvider;

اکنون می‌توانیم context را مورد استفاده قرار دهیم. در بخش‌های بعدی کامپوننت‌ها را ایجاد خواهیم کرد.

هوک UseContext

هوک UseContext یک ابزار حیاتی در توسعه React برای مدیریت کارآمد state و انتقال داده بین کامپوننت‌ها است. این هوک به کامپوننت‌ها کمک می‌کند تا بدون این که props را از کامپوننت‌های واسطه عبور دهند، به مقادیر context دسترسی پیدا کنند.

هنگامی که هوک UseContext با تایپ اسکریپت استفاده می‌شود، یک لایه اضافی از type safety به برنامه می‌افزاید و اطمینان حاصل می‌کند که تایپ‌های صحیح در سراسر برنامه مورد استفاده قرار می‌گیرند. این هوک بخشی از React Hooks API است و مقادیر را از React Context مصرف می‌کند و مقدار context فعلی را برای آن context برمی‌گرداند.

در بخش بعدی نحوه import کردن و استفاده از هوک UseContext را در کامپوننت خود بررسی خواهیم کرد.

نحوه ساخت کامپوننت‌ها و استفاده از context

در ادامه یک کامپوننت فرم داریم که به ما این امکان را می‌دهد تا داده‌های وارد شده توسط کاربر را با استفاده از هوک useState دیریت کنیم. هنگامی که داده‌های فرم را دریافت می‌کنیم، از تابع saveTodo که از آبجکت context به دست آمده است، برای افزودن یک to-do جدید استفاده می‌کنیم:

// components/AddTodo.tsx
import * as React from 'react';
import { TodoContext } from '../context/todoContext';
import { TodoContextType, ITodo } from '../@types/todo';

const AddTodo: React.FC = () => {
  const { saveTodo } = React.useContext(TodoContext) as TodoContextType;
  const [formData, setFormData] = React.useState<ITodo | {}>();
  const handleForm = (e: React.FormEvent<HTMLInputElement>): void => {
    setFormData({
      ...formData,
      [e.currentTarget.id]: e.currentTarget.value,
    });
  };
  const handleSaveTodo = (e: React.FormEvent, formData: ITodo | any) => {
    e.preventDefault();
    saveTodo(formData);
  };
  return (
    <form className="Form" onSubmit={(e) => handleSaveTodo(e, formData)}>
      <div>
        <div>
          <label htmlFor="name">Title</label>
          <input onChange={handleForm} type="text" id="title" />
        </div>
        <div>
          <label htmlFor="description">Description</label>
          <input onChange={handleForm} type="text" id="description" />
        </div>
      </div>
      <button disabled={formData === undefined ? true : false}>Add Todo</button>
    </form>
  );
};
export default AddTodo;

باید به این نکته توجه داشته باشیم که برای جلوگیری از خطاهای تایپ اسکریپت، از typecasting در هوک useContext استفاده می‌کنیم، زیرا context در ابتدا null خواهد بود:

// components/Todo.tsx
import * as React from 'react';
import { ITodo } from '../@types/todo';

type Props = {
  todo: ITodo;
  updateTodo: (id: number) => void;
};

const Todo: React.FC<Props> = ({ todo, updateTodo }) => {
  const checkTodo: string = todo.status ? `line-through` : '';
  return (
    <div className="Card">
      <div className="Card--text">
        <h1 className={checkTodo}>{todo.title}</h1>
        <span className={checkTodo}>{todo.description}</span>
      </div>
      <button onClick={() => updateTodo(todo.id)} className={todo.status ? `hide-button` : 'Card--button'}>
        Complete
      </button>
    </div>
  );
};
export default Todo;

همانطور که در اینجا می بینیم، ما یک کامپوننت نمایشی داریم که یک to-do منفرد را نشان می‌دهد. این کامپوننت آبجکت todo و تابع updateTodo را برای به‌روزرسانی آبجکت به عنوان پارامتر دریافت می‌کند که باید با تایپ Props، که در بالا تعریف شده است مطابقت داشته باشند:

// containers/Todos.tsx
import * as React from 'react';
import { TodoContextType, ITodo } from '../@types/todo';
import { TodoContext } from '../context/todoContext';
import Todo from '../components/Todo';

const Todos = () => {
  const { todos, updateTodo } = React.useContext(TodoContext) as TodoContextType;
  return (
    <>
      {todos.map((todo: ITodo) => (
        <Todo key={todo.id} updateTodo={updateTodo} todo={todo} />
      ))}
    </>
  );
};

export default Todos;

این کامپوننت لیستی از تسک‌هایی که انجام شده‌اند را هنگام لود صفحه به نمایش می‌گذارد و todos و تابع updateTodo را از to-do context دریافت می‌کند. در مرحله بعد، با ایجاد loop بر روی آرایه، آبجکتی را که می‌خواهیم نشان دهیم به کامپوننت Todo منتقل می‌کنیم.

از این مرحله به بعد، می‌توانیم to-do context را در فایل App.tsx برای تکمیل ساخت برنامه ارائه کنیم. بنابراین، در قسمت بعدی از context provider استفاده می‌کنیم.

ارائه context 

// App.tsx
import * as React from 'react'
import TodoProvider from './context/todoContext'
import Todos from './containers/Todos'
import AddTodo from './components/AddTodo'
import './styles.css'

export default function App() {
  return (
    <TodoProvider>
      <main className='App'>
        <h1>My Todos</h1>
        <AddTodo />
        <Todos />
      </main>
    </TodoProvider>
  )
}

در کدی که داریم، کامپوننت TodoProvider را import می‌کنیم و مصرف‌کننده‌های to-do context را درون آن قرار می‌دهیم. اکنون می‌توانیم به آرایه todos و تابع افزودن یا به‌روزرسانی یک to-do با استفاده از هوک useContext در سایر کامپوننت‌ها دسترسی داشته باشیم.

می‌توانیم پروژه را در ترمینال باز کنیم و یکی از دستورات زیر را اجرا نماییم:

yarn start

npm start

اگر همه چیز به درستی کار کند، می‌توانیم خروجی را در آدرس http://localhost:3000 در مرورگر ببینیم.

پیاده سازی React Context reducer با تایپ اسکریپت

هنگام مدیریت stateهای مشترک پیچیده در React Context، ترکیب هوک useReducer با تایپ اسکریپت به مدیریت state در سطح بالاتر و اشتراک‌گذاری آن در بین کامپوننت‌ها کمک می‌کند و استحکام کد و تجربیات توسعه‌دهنده را افزایش می‌دهد.

این مزایا شامل type safety، تکمیل خودکار کد و پشتیبانی از refactoring است. استفاده از هوک useReducer همچنین به متمرکز کردن تغییرات state کمک می‌کند و یک انتقال state قابل پیش‌بینی را تضمین می‌نماید.

این بار برنامه خود را با استفاده از context reducer همراه با تایپ اسکریپت بازنویسی می‌کنیم. ابتدا تایپ‌های action برای reducer را تعریف می‌کنیم:

// @types/todo.d.ts
export type TodoAction =
| { type: 'ADD_TODO'; payload: ITodo }
| { type: 'UPDATE_TODO'; payload: number };

سپس یک تابع reducer ایجاد می‌کنیم که تغییرات state را براساس actionهایی که ارسال شده‌اند، مدیریت می‌کند. در این مثال، actionها شامل اضافه کردن یک todoجدید یا به‌روزرسانی state یک تسک موجود است:

// reducers/todoReducer.ts
export const todoReducer = (state: ITodo[], action: TodoAction): ITodo[] => {
    switch (action.type) {
      case 'ADD_TODO':
        return [...state, action.payload];

      case 'UPDATE_TODO':
        return state.map((todo) =>
          todo.id === action.payload ? { ...todo, status: true } : todo
        );

      default:
        return state;
    }
};

اکنون در todoContext که داریم، state اولیه و تابع reducer را به آن ارائه می‌کنیم:

// context/todoContext.tsx
import * as React from 'react';
import { ITodo, TodoAction } from '../@types/todo';
import { todoReducer } from '../reducers/todoReducer';

export const TodoContext = React.createContext<{
  todos: ITodo[];
  dispatch: React.Dispatch<TodoAction>;
} | null>(null);

// TodoProvider component with the useReducer hook
const TodoProvider: React.FC<{children: React.ReactNode}> = ({ children }) => {
  const [todos, dispatch] = React.useReducer(todoReducer, [
    {
      id: 1,
      title: 'post 1',
      description: 'this is a description',
      status: false,
    },
    {
      id: 2,
      title: 'post 2',
      description: 'this is a description',
      status: true,
    },
  ]);
  return (
    <TodoContext.Provider value={{ todos, dispatch }}>
      {children}
    </TodoContext.Provider>
  );
};
export default TodoProvider;

در این مرحله، برنامه Reactای که داریم با استفاده از تایپ اسکریپت و به کمک context reducer پیکربندی شده است که type safety و یک رویکرد ساختاریافته برای مدیریت state سراسری را تضمین می‌کند.

کد موجود در فایل AddTodo.tsx را با کد زیر به‌روزرسانی می‌کنیم:

// components/AddTodo.tsx
import * as React from 'react';
import { TodoContext } from '../context/todoContext';
import { ITodo } from '../@types/todo';

const AddTodo: React.FC = () => {
  const { dispatch } = React.useContext(TodoContext)!;
  const [formData, setFormData] = React.useState<ITodo | {}>();

  const handleForm = (e: React.FormEvent<HTMLInputElement>): void => {
    setFormData({
      ...formData,
      [e.currentTarget.id]: e.currentTarget.value,
    });
  };

  const handleSaveTodo = (e: React.FormEvent, formData: ITodo | any) => {
    e.preventDefault();
    dispatch({ type: 'ADD_TODO', payload: formData });
  };

  return (
    <form className="Form" onSubmit={(e) => handleSaveTodo(e, formData)}>
      <div>
        <div>
          <label htmlFor="name">Title</label>
          <input onChange={handleForm} type="text" id="title" />
        </div>
        <div>
          <label htmlFor="description">Description</label>
          <input onChange={handleForm} type="text" id="description" />
        </div>
      </div>
      <button disabled={formData === undefined ? true : false}>Add Todo</button>
    </form>
  );
};
export default AddTodo;

در این مثال، برای دریافت تابع dispatch از TodoContext، از هوک useContext استفاده می‌کنیم. سپس کامپوننتی که داریم از تابع dispatch برای ارائه قابلیت افزودن تسک‌های جدید استفاده می‌کند.

کد موجود در فایل Todo.tsxرا با کد زیر به‌روزرسانی می‌کنیم:

// components/Todo.tsx
import * as React from 'react';
import { ITodo } from '../@types/todo';

type Props = {
  todo: ITodo;
  updateTodo: () => void;
};
const Todo: React.FC<Props> = ({ todo, updateTodo }) => {
  const checkTodo: string = todo.status ? `line-through` : '';

  return (
    <div className="Card">
      <div className="Card--text">
        <h1 className={checkTodo}>{todo.title}</h1>
        <span className={checkTodo}>{todo.description}</span>
      </div>
      <button onClick={() => updateTodo()} className={todo.status ? `hide-button` : 'Card--button'}>
        Complete
      </button>
    </div>
  );
};
export default Todo;

در این قسمت کامپوننتی که داریم، آبجکت todoرا دریافت می‌کند و آن را به عنوان پارامترهایی که باید با تایپ Propsتعریف شده مطابقت داشته باشد به‌روزرسانی می‌کند:

// containers/Todos.tsx
import * as React from 'react';
import { ITodo } from '../@types/todo';
import { TodoContext } from '../context/todoContext';
import Todo from '../components/Todo';
const Todos = () => {
  const { todos, dispatch } = React.useContext(TodoContext)!;
  return (
    <>
      {todos.map((todo: ITodo) => (
        <Todo key={todo.id} updateTodo={() => dispatch({ type: 'UPDATE_TODO', payload: todo.id })} todo={todo} />
      ))}
    </>
  );
};
export default Todos;

لیستی از todosرا نمایش می‌دهیم و با استفاده از تابع dispatchکه از TodoContextدریافت می‌کنیم، todo را به‌روزرسانی می‌نماییم.

باید مطمئن شویم که محتویات موجود در فایل App.tsx را داخل TodoProviderقرار داده‌ایم تا بتوانیم به آرایه todosو تابع افزودن یا به‌روزرسانی یک todo با استفاده از هوک useContextدر سایر کامپوننت‌ها دسترسی داشته باشیم:

// App.tsx
import * as React from 'react'
import TodoProvider from './context/todoContext'
import Todos from './containers/Todos'
import AddTodo from './components/AddTodo'
import './styles.css'

export default function App() {
  return (
    <TodoProvider>
      <main className='App'>
        <h1>My Todos</h1>
        <AddTodo />
        <Todos />
      </main>
    </TodoProvider>
  )
}

بررسی مبحث Theming با Context API

در این بخش، قصد داریم تا نگاهی به یکی دیگر از کاربردهای Context API یعنی مبحث theming بیاندازیم:

// @types/theme.d.ts
export type Theme = 'light' | 'dark';
export type ThemeContextType = {
  theme: Theme;
  changeTheme: (theme: Theme) => void;
};
};

ما تایپ‌های لازم برای پیاده‌سازی theming را ایجاد می‌کنیم: Themeحالت‌های ممکن را برای تم مشخص می‌کند وThemeContextTypeویژگی‌هایی را تعیین می‌کند که در context تم در هنگام استفاده از آن در دسترس خواهند بود.

در مرحله بعد، یک فایل themeContext.tsxایجاد می‌کنیم که context تم خام و provider آن را export می‌کند:

// context/themeContext.tsx
import * as React from 'react';
import { Theme, ThemeContextType } from '../@types/theme';

export const ThemeContext = React.createContext<ThemeContextType | null>(null);

const ThemeProvider: React.FC<{children: React.ReactNode}> = ({ children }) => {
  const [themeMode, setThemeMode] = React.useState<Theme>('light');
  return (
    <ThemeContext.Provider value={{ theme: themeMode, changeTheme: setThemeMode }}>
      {children}
    </ThemeContext.Provider>
  );
};

export default ThemeProvider;

پس از ایجاد context، یک کامپوننت ThemeWrapperمی‌سازیم که هم آن را استفاده می‌کند و هم تم را در برنامه تغییر می‌دهد:

// components/ThemeWrapper.tsx
import React from 'react';
import { ThemeContextType, Theme } from '../@types/theme';
import { ThemeContext } from '../context/themeContext';

const ThemeWrapper: React.FC<{children: React.ReactNode}> = ({ children }) => {
  const { theme, changeTheme } = React.useContext(ThemeContext) as ThemeContextType;
  const handleThemeChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
    changeTheme(event.target.value as Theme);
  };

  return (
    <div data-theme={theme}>
      <select name="toggleTheme" onChange={handleThemeChange}>
        <option value="light">Light</option>
        <option value="dark">Dark</option>
      </select>
      {children}
    </div>
  );
};
export default ThemeWrapper;

با آماده شدن ThemeWrapper، اکنون می‌توانیم آن را در App.tsxکه داریم import کنیم و مورد آزمایش قرار دهیم:

// App.tsx
import * as React from 'react';
import TodoProvider from './context/todoContext';
import ThemeProvider from './context/themeContext';
import Todos from './containers/Todos';
import AddTodo from './components/AddTodo';
import ThemeWrapper from './components/ThemeWrapper';
import './styles.css';

export default function App() {
  return (
    <ThemeProvider>
      <TodoProvider>
        <ThemeWrapper>
          <main className="App">
            <h1>My Todos</h1>
            <AddTodo />
            <Todos />
          </main>
        </ThemeWrapper>
      </TodoProvider>
    </ThemeProvider>
  );
}

هنگامی که ویژگی data-themeتوسط change handler المنت انتخابی اصلاح شد، فایل CSSای که از پیش نوشته شده است بقیه موارد را بر عهده می‌گیرد. می‌توانیم سورس کد کامل را در این لینک مشاهده نماییم.

مشکلات رایج تایپ اسکریپت هنگام استفاده از React Context

با وجود مزایای فراوانی که React Context دارد، اما توسعه‌دهندگان اغلب در هنگام استفاده از آن همراه با تایپ اسکریپت با چالش‌هایی مواجه می‌شوند.

مشکلات Type assertion

تایپ اسکریپت می‌تواند برای استنباط صحیح تایپ‌ها، به خصوص با ساختارهای پیچیده یا contextهای تودرتو مشکل داشته باشد. برای بهبود type safety، باید تایپ‌ها را به صراحت برای مقادیر context تعریف کنیم یا این که از کلمه کلیدی asبرای type assertion استفاده نماییم:

// Using the context with type assertion
const todoContext = useContext(TodoContext) as TodoContextType;

// type assertion
export const TodoContext = React.createContext<TodoContextType>();

مدیریت مقادیر null یا undefined

در شرایطی که مقدار پیش‌فرض context به متغیرهای دیگر بستگی دارد یا این که در طول ایجاد context نمی‌توانیم آن را تعیین کنیم، لازم است مقادیر undefinedیا nullرا به عنوان مقدار پیش‌فرض به context اختصاص دهیم؛ در غیر این صورت تایپ اسکریپت خطا تایپ ایجاد می‌کند.

این مشکل را می‌توانیم با تعریف تایپ context به‌عنوان مقدار undefined یا null و یا استفاده از optional chaining یا non-null assertionها برای داشتن دسترسی safe، حل کنیم:

// defining the context type as nullable
export const TodoContext = React.createContext<TodoContextType | null>(null);

// non-null assertions
const { todos, dispatch } = React.useContext(TodoContext)!;

// using optional chaining
const safeValue = todos?.property;

راه حل دیگر برای رفع این مشکل استفاده از تابع کمکی می‌باشد:

// context/todoContext.tsx
export const useTodoContext = () => {
  const context = useContext(TodoContext);

  if (!context) {
    throw new Error('useTodoContext must be used inside the TodoProvider');
  }

  return context;
};

این تابع از هوک useContextبرای بدست آوردن مقدار فعلی TodoContextاستفاده می‌کند. اگر این مقدار null یا undefined باشد، خطایی را نشان می‌دهد که نیاز به استفاده از تابع useTodoContext در TodoProvider را بیان می‌کند. اگر این مقدار در دسترس باشد، با اطمینان از اینکه کامپوننت مصرف‌کننده، مقدار context را دریافت می‌کند و تایپ اسکریپت می‌تواند تایپ‌های صحیح را استنباط نماید آن را return می‌کند:

const TodoList: React.FC = () => {
  // Use the useTodoContext helper to obtain the context value safely
  const { todos, dispatch } = useTodoContext();

  // ... rest of the component code
};

اکنون، todos و dispatch دارای تایپ‌های صحیح هستند و مشکلات null یا undefined مدیریت می‌شوند.

بررسی type safety برای تابع Reducer

اطمینان از type safety در توابع reducer، به ویژه هنگام کار با ساختارهای state پیچیده می‌تواند دشوار باشد.

استفاده از unionهای discriminated برای مدیریت شرایط action مختلف و ایجاد تعاریف روشن و دقیق برای تایپ‌های action، موضوع type safety را در reducer تضمین می‌کند و به تایپ اسکریپت در تفسیر مناسب تایپ‌ها کمک می‌کند:

type TodoAction =
  | { type: 'ADD_TODO'; payload: ITodo }
  | { type: 'UPDATE_TODO'; payload: number };

استفاده بیش از حد از context و عملکرد

اگر از context استفاده بیش از اندازه داشته باشیم این موضوع می‌تواند تأثیر منفی بر روی رندرهای مجدد و عملکرد داشته باشد:

import React, { useContext } from 'react';

// Counter 1: Renders a counter using context
const Counter1 = () => {
  const counter = useContext(CounterContext);
  console.log('Counter1 rendered');

  return (
    <div>
      <p>Counter1 Counter: {counter}</p>
    </div>
  );
};

// Counter 2: Renders another counter using context
const Counter2 = () => {
  const counter = useContext(CounterContext);
  console.log('Counter2 rendered');

  return (
    <div>
      <p>Counter2 Counter: {counter}</p>
    </div>
  );
};

const App = () => {
  const [counter, setCounter] = useState(0);
  console.log('App rendered');

  return (
    <CounterContext.Provider value={counter}>
      <div>
        <button onClick={() => setCounter((prevCounter) => prevCounter + 1)}>
          Increment Counter
        </button>
        <Counter1 />
        <Counter2 />
      </div>
    </CounterContext.Provider>
  );
};

export default App;

در مثالی که داریم دو کامپوننت Counter1 و Counter2 از یک context به نام CounterContext برای دسترسی به یک مقدار شمارنده استفاده می‌کنند. کامپوننت App هر دو کامپوننت را رندر می‌کند، اما وقتی شمارنده افزایش می‌یابد، هر دو کامپوننت دوباره رندر می‌شوند. این موضوع یک مشکل رایج است که هنگام استفاده بیش از حد از context ایجاد می‌شود و احتمالاً در مبحث عملکرد برنامه هم مشکلاتی را ایجاد می‌کند.

برای برطرف کردن این مشکل، باید از context برای داده‌های سراسری یا deeply shared استفاده کنیم، راه‌حل‌های مدیریت state جایگزین مانند Redux را در نظر بگیریم، یا به‌روزرسانی‌های context را با استفاده از تکنیک‌های memoization بهینه کنیم:

onst Counter1 = React.memo(() => {
  const counter = useContext(CounterContext);
  console.log('Counter1 rendered');

  return (
    <div>
      <p>Counter1 Counter: {counter}</p>
    </div>
  );
});

const Counter2 = React.memo(() => {
  const counter = useContext(CounterContext);
  console.log('Counter2 rendered');

  return (
    <div>
      <p>Counter2 Counter: {counter}</p>
    </div>
  );
});

React.memo با جلوگیری از رندرهای مجدد غیرضروری در زمانی که props بدون تغییر باقی می‌مانند، کامپوننت‌ها را بهینه می‌کند و در نتیجه مشکلات عملکرد را که به دلیل به‌روزرسانی‌های بیش از حد context بود، کاهش می‌دهد.

جمع‌بندی

تایپ اسکریپت یک زبان برنامه نویسی عالی است که می‌تواند کد ما را بهبود ببخشد. در این مقاله سعی کردیم نحوه استفاده از تایپ اسکریپت با React Context و روش پیاده‌سازی React Context Reducer با تایپ اسکریپت را بررسی کنیم. همچنین به برخی از مشکلات رایجی که ممکن است توسعه‌دهندگان هنگام استفاده از React Context با تایپ اسکریپت با آن مواجه شوند، پرداختیم.