Ελληνικά

Εξερευνήστε προηγμένα πρότυπα για το React Context API, όπως σύνθετα components, δυναμικά contexts και τεχνικές βελτιστοποίησης απόδοσης για πολύπλοκη διαχείριση κατάστασης.

Προηγμένα Πρότυπα του React Context API για τη Διαχείριση Κατάστασης

Το React Context API παρέχει έναν ισχυρό μηχανισμό για τον διαμοιρασμό της κατάστασης σε όλη την εφαρμογή σας χωρίς prop drilling. Ενώ η βασική χρήση είναι απλή, η αξιοποίηση του πλήρους δυναμικού του απαιτεί την κατανόηση προηγμένων προτύπων που μπορούν να διαχειριστούν πολύπλοκα σενάρια διαχείρισης κατάστασης. Αυτό το άρθρο εξερευνά αρκετά από αυτά τα πρότυπα, προσφέροντας πρακτικά παραδείγματα και χρήσιμες γνώσεις για να αναβαθμίσετε την ανάπτυξη με React.

Κατανόηση των Περιορισμών του Βασικού Context API

Πριν εμβαθύνουμε στα προηγμένα πρότυπα, είναι κρίσιμο να αναγνωρίσουμε τους περιορισμούς του βασικού Context API. Ενώ είναι κατάλληλο για απλή, καθολικά προσβάσιμη κατάσταση, μπορεί να γίνει δυσκίνητο και αναποτελεσματικό για πολύπλοκες εφαρμογές με συχνά μεταβαλλόμενη κατάσταση. Κάθε component που καταναλώνει ένα context επαναποδίδεται (re-renders) κάθε φορά που η τιμή του context αλλάζει, ακόμα και αν το component δεν εξαρτάται από το συγκεκριμένο τμήμα της κατάστασης που ενημερώθηκε. Αυτό μπορεί να οδηγήσει σε προβλήματα απόδοσης (performance bottlenecks).

Πρότυπο 1: Σύνθετα Components (Compound Components) με Context

Το πρότυπο των Σύνθετων Components (Compound Component) ενισχύει το Context API δημιουργώντας μια σουίτα σχετικών components που μοιράζονται σιωπηρά την κατάσταση και τη λογική μέσω ενός context. Αυτό το πρότυπο προωθεί την επαναχρησιμοποίηση και απλοποιεί το API για τους καταναλωτές. Αυτό επιτρέπει την ενθυλάκωση πολύπλοκης λογικής με απλή υλοποίηση.

Παράδειγμα: Ένα Component Καρτελών (Tab Component)

Ας το επεξηγήσουμε με ένα component Καρτελών (Tab component). Αντί να περνάμε props κάτω από πολλαπλά επίπεδα, τα Tab components επικοινωνούν σιωπηρά μέσω ενός κοινόχρηστου context.

// TabContext.js
import React, { createContext, useContext, useState, ReactNode } from 'react';

interface TabContextType {
  activeTab: string;
  setActiveTab: (tab: string) => void;
}

const TabContext = createContext(undefined);

interface TabProviderProps {
  children: ReactNode;
  defaultTab: string;
}

export const TabProvider: React.FC = ({ children, defaultTab }) => {
  const [activeTab, setActiveTab] = useState(defaultTab);

  const value: TabContextType = {
    activeTab,
    setActiveTab,
  };

  return {children};
};

export const useTabContext = () => {
  const context = useContext(TabContext);
  if (!context) {
    throw new Error('useTabContext must be used within a TabProvider');
  }
  return context;
};

// TabList.js
import React, { ReactNode } from 'react';

interface TabListProps {
  children: ReactNode;
}

export const TabList: React.FC = ({ children }) => {
  return 
{children}
; }; // Tab.js import React, { ReactNode } from 'react'; import { useTabContext } from './TabContext'; interface TabProps { label: string; children: ReactNode; } export const Tab: React.FC = ({ label, children }) => { const { activeTab, setActiveTab } = useTabContext(); const isActive = activeTab === label; const handleClick = () => { setActiveTab(label); }; return ( ); }; // TabPanel.js import React, { ReactNode } from 'react'; import { useTabContext } from './TabContext'; interface TabPanelProps { label: string; children: ReactNode; } export const TabPanel: React.FC = ({ label, children }) => { const { activeTab } = useTabContext(); const isActive = activeTab === label; return ( ); };
// Χρήση
import { TabProvider, TabList, Tab, TabPanel } from './components/Tabs';

function App() {
  return (
    
      
        Tab 1
        Tab 2
        Tab 3
      
      Content for Tab 1
      Content for Tab 2
      Content for Tab 3
    
  );
}

export default App;

Οφέλη:

Πρότυπο 2: Δυναμικά Contexts

Σε ορισμένα σενάρια, μπορεί να χρειαστείτε διαφορετικές τιμές context ανάλογα με τη θέση του component στο δέντρο των components ή άλλους δυναμικούς παράγοντες. Τα δυναμικά contexts σας επιτρέπουν να δημιουργείτε και να παρέχετε τιμές context που ποικίλλουν ανάλογα με συγκεκριμένες συνθήκες.

Παράδειγμα: Δημιουργία Θεμάτων (Theming) με Δυναμικά Contexts

Σκεφτείτε ένα σύστημα θεμάτων όπου θέλετε να παρέχετε διαφορετικά θέματα με βάση τις προτιμήσεις του χρήστη ή την ενότητα της εφαρμογής στην οποία βρίσκεται. Μπορούμε να κάνουμε ένα απλοποιημένο παράδειγμα με φωτεινό και σκούρο θέμα.

// ThemeContext.js
import React, { createContext, useContext, useState, ReactNode } from 'react';

interface Theme {
  background: string;
  color: string;
}

interface ThemeContextType {
  theme: Theme;
  toggleTheme: () => void;
}

const defaultTheme: Theme = {
    background: 'white',
    color: 'black'
};

const darkTheme: Theme = {
    background: 'black',
    color: 'white'
};

const ThemeContext = createContext({
    theme: defaultTheme,
    toggleTheme: () => {}
});

interface ThemeProviderProps {
  children: ReactNode;
}

export const ThemeProvider: React.FC = ({ children }) => {
  const [isDarkTheme, setIsDarkTheme] = useState(false);
  const theme = isDarkTheme ? darkTheme : defaultTheme;

  const toggleTheme = () => {
    setIsDarkTheme(!isDarkTheme);
  };

  const value: ThemeContextType = {
    theme,
    toggleTheme,
  };

  return {children};
};

export const useTheme = () => {
  return useContext(ThemeContext);
};
// Χρήση
import { useTheme, ThemeProvider } from './ThemeContext';

function MyComponent() {
  const { theme, toggleTheme } = useTheme();

  return (
    

This is a themed component.

); } function App() { return ( ); } export default App;

Σε αυτό το παράδειγμα, το ThemeProvider καθορίζει δυναμικά το θέμα με βάση την κατάσταση isDarkTheme. Τα components που χρησιμοποιούν το useTheme hook θα επαναποδοθούν αυτόματα όταν αλλάξει το θέμα.

Πρότυπο 3: Context με useReducer για Πολύπλοκη Κατάσταση

Για τη διαχείριση πολύπλοκης λογικής κατάστασης, ο συνδυασμός του Context API με το useReducer είναι μια εξαιρετική προσέγγιση. Το useReducer παρέχει έναν δομημένο τρόπο ενημέρωσης της κατάστασης βάσει ενεργειών (actions), και το Context API σας επιτρέπει να μοιράζεστε αυτήν την κατάσταση και τη συνάρτηση dispatch σε ολόκληρη την εφαρμογή σας.

Παράδειγμα: Μια Απλή Λίστα Εργασιών (Todo List)

// TodoContext.js
import React, { createContext, useContext, useReducer, ReactNode } from 'react';

interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

interface TodoState {
  todos: Todo[];
}

type TodoAction = 
  | { type: 'ADD_TODO'; text: string } 
  | { type: 'TOGGLE_TODO'; id: number } 
  | { type: 'DELETE_TODO'; id: number };

interface TodoContextType {
  state: TodoState;
  dispatch: React.Dispatch;
}

const initialState: TodoState = {
  todos: [],
};

const todoReducer = (state: TodoState, action: TodoAction): TodoState => {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [...state.todos, { id: Date.now(), text: action.text, completed: false }],
      };
    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map((todo) =>
          todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
        ),
      };
    case 'DELETE_TODO':
      return {
        ...state,
        todos: state.todos.filter((todo) => todo.id !== action.id),
      };
    default:
      return state;
  }
};

const TodoContext = createContext(undefined);

interface TodoProviderProps {
  children: ReactNode;
}

export const TodoProvider: React.FC = ({ children }) => {
  const [state, dispatch] = useReducer(todoReducer, initialState);

  const value: TodoContextType = {
    state,
    dispatch,
  };

  return {children};
};

export const useTodo = () => {
  const context = useContext(TodoContext);
  if (!context) {
    throw new Error('useTodo must be used within a TodoProvider');
  }
  return context;
};
// Χρήση
import { useTodo, TodoProvider } from './TodoContext';

function TodoList() {
  const { state, dispatch } = useTodo();

  return (
    
    {state.todos.map((todo) => (
  • {todo.text}
  • ))}
); } function AddTodo() { const { dispatch } = useTodo(); const [text, setText] = React.useState(''); const handleSubmit = (e) => { e.preventDefault(); dispatch({ type: 'ADD_TODO', text }); setText(''); }; return (
setText(e.target.value)} />
); } function App() { return ( ); } export default App;

Αυτό το πρότυπο συγκεντρώνει τη λογική διαχείρισης της κατάστασης μέσα στον reducer, καθιστώντας ευκολότερη την κατανόηση και τον έλεγχο. Τα components μπορούν να αποστέλλουν ενέργειες (dispatch actions) για να ενημερώσουν την κατάσταση χωρίς να χρειάζεται να τη διαχειρίζονται απευθείας.

Πρότυπο 4: Βελτιστοποιημένες Ενημερώσεις Context με `useMemo` και `useCallback`

Όπως αναφέρθηκε προηγουμένως, ένα βασικό ζήτημα απόδοσης με το Context API είναι οι περιττές επαναποδόσεις (re-renders). Η χρήση των useMemo και useCallback μπορεί να αποτρέψει αυτές τις επαναποδόσεις, διασφαλίζοντας ότι ενημερώνονται μόνο τα απαραίτητα μέρη της τιμής του context και ότι οι αναφορές των συναρτήσεων παραμένουν σταθερές.

Παράδειγμα: Βελτιστοποίηση ενός Theme Context

// OptimizedThemeContext.js
import React, { createContext, useContext, useState, useMemo, useCallback, ReactNode } from 'react';

interface Theme {
  background: string;
  color: string;
}

interface ThemeContextType {
  theme: Theme;
  toggleTheme: () => void;
}

const defaultTheme: Theme = {
    background: 'white',
    color: 'black'
};

const darkTheme: Theme = {
    background: 'black',
    color: 'white'
};

const ThemeContext = createContext({
    theme: defaultTheme,
    toggleTheme: () => {}
});

interface ThemeProviderProps {
  children: ReactNode;
}

export const ThemeProvider: React.FC = ({ children }) => {
  const [isDarkTheme, setIsDarkTheme] = useState(false);
  const theme = isDarkTheme ? darkTheme : defaultTheme;

  const toggleTheme = useCallback(() => {
    setIsDarkTheme(!isDarkTheme);
  }, [isDarkTheme]);

  const value: ThemeContextType = useMemo(() => ({
    theme,
    toggleTheme,
  }), [theme, toggleTheme]);

  return {children};
};

export const useTheme = () => {
  return useContext(ThemeContext);
};

Εξήγηση:

Χωρίς το useCallback, η συνάρτηση toggleTheme θα δημιουργούνταν εκ νέου σε κάθε render του ThemeProvider, προκαλώντας την αλλαγή της value και ενεργοποιώντας επαναποδόσεις σε οποιοδήποτε component που την καταναλώνει, ακόμη και αν το ίδιο το θέμα δεν είχε αλλάξει. Το useMemo διασφαλίζει ότι μια νέα value δημιουργείται μόνο όταν αλλάζουν οι εξαρτήσεις της (theme ή toggleTheme).

Πρότυπο 5: Επιλογείς Context (Context Selectors)

Οι επιλογείς context επιτρέπουν στα components να εγγράφονται μόνο σε συγκεκριμένα τμήματα της τιμής του context. Αυτό αποτρέπει τις περιττές επαναποδόσεις όταν αλλάζουν άλλα μέρη του context. Βιβλιοθήκες όπως το `use-context-selector` ή προσαρμοσμένες υλοποιήσεις μπορούν να χρησιμοποιηθούν για την επίτευξη αυτού του στόχου.

Παράδειγμα με Χρήση Προσαρμοσμένου Επιλογέα Context

// useCustomContextSelector.js
import { useContext, useState, useRef, useEffect } from 'react';

function useCustomContextSelector(
  context: React.Context,
  selector: (value: T) => S
): S {
  const value = useContext(context);
  const [selected, setSelected] = useState(() => selector(value));
  const latestSelector = useRef(selector);
  latestSelector.current = selector;

  useEffect(() => {
    let didUnmount = false;
    let lastSelected = selected;

    const subscription = () => {
      if (didUnmount) {
        return;
      }
      const nextSelected = latestSelector.current(value);
      if (!Object.is(lastSelected, nextSelected)) {
        lastSelected = nextSelected;
        setSelected(nextSelected);
      }
    };

    // Συνήθως θα κάνατε εγγραφή στις αλλαγές του context εδώ. Καθώς αυτό είναι ένα απλοποιημένο
    // παράδειγμα, θα καλέσουμε την subscription αμέσως για αρχικοποίηση.
    subscription();

    return () => {
      didUnmount = true;
      // Απεγγραφή από τις αλλαγές του context εδώ, εάν ισχύει.
    };
  }, [value]); // Επανεκτέλεση του effect όποτε αλλάζει η τιμή του context

  return selected;
}

export default useCustomContextSelector;
// ThemeContext.js (Απλοποιημένο για συντομία)
import React, { createContext, useState, ReactNode } from 'react';

interface Theme {
  background: string;
  color: string;
}

interface ThemeContextType {
  theme: Theme;
  setTheme: (newTheme: Theme) => void; 
}

const ThemeContext = createContext(undefined);

interface ThemeProviderProps {
  children: ReactNode;
  initialTheme: Theme;
}

export const ThemeProvider: React.FC = ({ children, initialTheme }) => {
  const [theme, setTheme] = useState(initialTheme);

  const value: ThemeContextType = {
    theme,
    setTheme
  };

  return {children};
};

export const useThemeContext = () => {
    const context = React.useContext(ThemeContext);
    if (!context) {
        throw new Error("useThemeContext must be used within a ThemeProvider");
    }
    return context;
};

export default ThemeContext;
// Χρήση
import useCustomContextSelector from './useCustomContextSelector';
import ThemeContext, { ThemeProvider, useThemeContext } from './ThemeContext';

function BackgroundComponent() {
  const background = useCustomContextSelector(ThemeContext, (context) => context.theme.background);
  return 
Background
; } function ColorComponent() { const color = useCustomContextSelector(ThemeContext, (context) => context.theme.color); return
Color
; } function App() { const { theme, setTheme } = useThemeContext(); const toggleTheme = () => { setTheme({ background: theme.background === 'white' ? 'black' : 'white', color: theme.color === 'black' ? 'white' : 'black' }); }; return ( ); } export default App;

Σε αυτό το παράδειγμα, το BackgroundComponent επαναποδίδεται μόνο όταν αλλάζει η ιδιότητα background του θέματος, και το ColorComponent επαναποδίδεται μόνο όταν αλλάζει η ιδιότητα color. Αυτό αποφεύγει τις περιττές επαναποδόσεις όταν αλλάζει ολόκληρη η τιμή του context.

Πρότυπο 6: Διαχωρισμός Ενεργειών (Actions) από την Κατάσταση (State)

Για μεγαλύτερες εφαρμογές, εξετάστε το ενδεχόμενο να διαχωρίσετε την τιμή του context σε δύο ξεχωριστά contexts: ένα για την κατάσταση (state) και ένα άλλο για τις ενέργειες (dispatch functions). Αυτό μπορεί να βελτιώσει την οργάνωση του κώδικα και τη δυνατότητα ελέγχου (testability).

Παράδειγμα: Λίστα Εργασιών με Ξεχωριστά Contexts για Κατάσταση και Ενέργειες

// TodoStateContext.js
import React, { createContext, useContext, useReducer, ReactNode } from 'react';

interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

interface TodoState {
  todos: Todo[];
}

const initialState: TodoState = {
  todos: [],
};

const TodoStateContext = createContext(initialState);

interface TodoStateProviderProps {
  children: ReactNode;
}

export const TodoStateProvider: React.FC = ({ children }) => {
  const [state] = useReducer(todoReducer, initialState);

  return {children};
};

export const useTodoState = () => {
  return useContext(TodoStateContext);
};

// TodoActionContext.js
import React, { createContext, useContext, Dispatch, ReactNode } from 'react';

type TodoAction = 
  | { type: 'ADD_TODO'; text: string } 
  | { type: 'TOGGLE_TODO'; id: number } 
  | { type: 'DELETE_TODO'; id: number };

const TodoActionContext = createContext | undefined>(undefined);

interface TodoActionProviderProps {
    children: ReactNode;
}

export const TodoActionProvider: React.FC = ({children}) => {
    const [, dispatch] = useReducer(todoReducer, initialState);

    return {children};
};


export const useTodoDispatch = () => {
  const dispatch = useContext(TodoActionContext);
  if (!dispatch) {
    throw new Error('useTodoDispatch must be used within a TodoActionProvider');
  }
  return dispatch;
};

// todoReducer.js
export const todoReducer = (state: TodoState, action: TodoAction): TodoState => {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [...state.todos, { id: Date.now(), text: action.text, completed: false }],
      };
    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map((todo) =>
          todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
        ),
      };
    case 'DELETE_TODO':
      return {
        ...state,
        todos: state.todos.filter((todo) => todo.id !== action.id),
      };
    default:
      return state;
  }
};
// Χρήση
import { useTodoState, TodoStateProvider } from './TodoStateContext';
import { useTodoDispatch, TodoActionProvider } from './TodoActionContext';

function TodoList() {
  const state = useTodoState();

  return (
    
    {state.todos.map((todo) => (
  • {todo.text}
  • ))}
); } function TodoActions({ todo }) { const dispatch = useTodoDispatch(); return ( <> ); } function AddTodo() { const dispatch = useTodoDispatch(); const [text, setText] = React.useState(''); const handleSubmit = (e) => { e.preventDefault(); dispatch({ type: 'ADD_TODO', text }); setText(''); }; return (
setText(e.target.value)} />
); } function App() { return ( ); } export default App;

Αυτός ο διαχωρισμός επιτρέπει στα components να εγγράφονται μόνο στο context που χρειάζονται, μειώνοντας τις περιττές επαναποδόσεις. Επίσης, καθιστά ευκολότερο τον έλεγχο μονάδας (unit test) του reducer και κάθε component μεμονωμένα. Επιπλέον, η σειρά περιτύλιξης των providers έχει σημασία. Ο ActionProvider πρέπει να περιτυλίξει τον StateProvider.

Βέλτιστες Πρακτικές και Σκέψεις

Συμπέρασμα

Το React Context API είναι ένα ευέλικτο εργαλείο για τη διαχείριση κατάστασης. Κατανοώντας και εφαρμόζοντας αυτά τα προηγμένα πρότυπα, μπορείτε να διαχειριστείτε αποτελεσματικά την πολύπλοκη κατάσταση, να βελτιστοποιήσετε την απόδοση και να δημιουργήσετε πιο συντηρήσιμες και επεκτάσιμες εφαρμογές React. Θυμηθείτε να επιλέξετε το σωστό πρότυπο για τις συγκεκριμένες ανάγκες σας και να εξετάσετε προσεκτικά τις επιπτώσεις στην απόδοση από τη χρήση του context.

Καθώς το React εξελίσσεται, το ίδιο θα συμβαίνει και με τις βέλτιστες πρακτικές γύρω από το Context API. Η ενημέρωση για νέες τεχνικές και βιβλιοθήκες θα διασφαλίσει ότι είστε εξοπλισμένοι για να αντιμετωπίσετε τις προκλήσεις διαχείρισης κατάστασης της σύγχρονης ανάπτυξης web. Εξετάστε το ενδεχόμενο να εξερευνήσετε αναδυόμενα πρότυπα, όπως η χρήση του context με signals για ακόμη πιο λεπτομερή αντιδραστικότητα (reactivity).