Bahasa Indonesia

Jelajahi pola-pola lanjutan untuk React Context API, termasuk compound component, konteks dinamis, dan teknik optimasi performa untuk manajemen state yang kompleks.

Pola Lanjutan React Context API untuk Manajemen State

React Context API menyediakan mekanisme yang kuat untuk berbagi state di seluruh aplikasi Anda tanpa prop drilling. Meskipun penggunaan dasarnya cukup mudah, memanfaatkan potensi penuhnya memerlukan pemahaman pola-pola lanjutan yang dapat menangani skenario manajemen state yang kompleks. Artikel ini akan membahas beberapa pola tersebut, menawarkan contoh praktis dan wawasan yang dapat ditindaklanjuti untuk meningkatkan pengembangan React Anda.

Memahami Keterbatasan Context API Dasar

Sebelum mendalami pola-pola lanjutan, penting untuk menyadari keterbatasan dari Context API dasar. Meskipun cocok untuk state yang sederhana dan dapat diakses secara global, ia bisa menjadi tidak praktis dan tidak efisien untuk aplikasi kompleks dengan state yang sering berubah. Setiap komponen yang menggunakan sebuah konteks akan di-render ulang setiap kali nilai konteks berubah, bahkan jika komponen tersebut tidak bergantung pada bagian spesifik dari state yang diperbarui. Hal ini dapat menyebabkan kemacetan performa.

Pola 1: Compound Component dengan Konteks

Pola Compound Component menyempurnakan Context API dengan menciptakan serangkaian komponen terkait yang secara implisit berbagi state dan logika melalui sebuah konteks. Pola ini mempromosikan penggunaan kembali (reusability) dan menyederhanakan API bagi penggunanya. Hal ini memungkinkan logika yang kompleks untuk dienkapsulasi dengan implementasi yang sederhana.

Contoh: Komponen Tab

Mari kita ilustrasikan ini dengan komponen Tab. Alih-alih meneruskan props melalui beberapa lapisan, komponen Tab berkomunikasi secara implisit melalui konteks bersama.

// 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 ( ); };
// Usage
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;

Manfaat:

Pola 2: Konteks Dinamis

Dalam beberapa skenario, Anda mungkin memerlukan nilai konteks yang berbeda berdasarkan posisi komponen di dalam pohon komponen atau faktor dinamis lainnya. Konteks dinamis memungkinkan Anda untuk membuat dan menyediakan nilai konteks yang bervariasi berdasarkan kondisi tertentu.

Contoh: Theming dengan Konteks Dinamis

Pertimbangkan sistem tema di mana Anda ingin menyediakan tema yang berbeda berdasarkan preferensi pengguna atau bagian aplikasi yang sedang mereka gunakan. Kita dapat membuat contoh sederhana dengan tema terang dan gelap.

// 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);
};
// Usage
import { useTheme, ThemeProvider } from './ThemeContext';

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

  return (
    

This is a themed component.

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

Dalam contoh ini, ThemeProvider secara dinamis menentukan tema berdasarkan state isDarkTheme. Komponen yang menggunakan hook useTheme akan secara otomatis di-render ulang ketika tema berubah.

Pola 3: Konteks dengan useReducer untuk State yang Kompleks

Untuk mengelola logika state yang kompleks, menggabungkan Context API dengan useReducer adalah pendekatan yang sangat baik. useReducer menyediakan cara terstruktur untuk memperbarui state berdasarkan action, dan Context API memungkinkan Anda untuk berbagi state ini dan fungsi dispatch di seluruh aplikasi Anda.

Contoh: Daftar Todo Sederhana

// 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;
};
// Usage
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;

Pola ini memusatkan logika manajemen state di dalam reducer, membuatnya lebih mudah untuk dipahami dan diuji. Komponen dapat mengirimkan (dispatch) action untuk memperbarui state tanpa perlu mengelola state secara langsung.

Pola 4: Pembaruan Konteks yang Dioptimalkan dengan `useMemo` dan `useCallback`

Seperti yang disebutkan sebelumnya, pertimbangan performa utama dengan Context API adalah render ulang yang tidak perlu. Menggunakan useMemo dan useCallback dapat mencegah render ulang ini dengan memastikan bahwa hanya bagian yang diperlukan dari nilai konteks yang diperbarui, dan bahwa referensi fungsi tetap stabil.

Contoh: Mengoptimalkan Konteks Tema

// 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);
};

Penjelasan:

Tanpa useCallback, fungsi toggleTheme akan dibuat ulang pada setiap render dari ThemeProvider, menyebabkan value berubah dan memicu render ulang di komponen mana pun yang menggunakannya, bahkan jika temanya sendiri tidak berubah. useMemo memastikan value yang baru hanya dibuat ketika dependensinya (theme atau toggleTheme) berubah.

Pola 5: Context Selector

Context selector memungkinkan komponen untuk berlangganan (subscribe) hanya pada bagian tertentu dari nilai konteks. Hal ini mencegah render ulang yang tidak perlu ketika bagian lain dari konteks berubah. Pustaka seperti `use-context-selector` atau implementasi kustom dapat digunakan untuk mencapai ini.

Contoh Menggunakan Context Selector Kustom

// 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);
      }
    };

    // You would typically subscribe to context changes here. Since this is a simplified
    // example, we'll just call subscription immediately to initialize.
    subscription();

    return () => {
      didUnmount = true;
      // Unsubscribe from context changes here, if applicable.
    };
  }, [value]); // Re-run effect whenever the context value changes

  return selected;
}

export default useCustomContextSelector;
// ThemeContext.js (Simplified for brevity)
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;
// Usage
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;

Dalam contoh ini, BackgroundComponent hanya akan di-render ulang ketika properti background dari tema berubah, dan ColorComponent hanya akan di-render ulang ketika properti color berubah. Ini menghindari render ulang yang tidak perlu ketika seluruh nilai konteks berubah.

Pola 6: Memisahkan Action dari State

Untuk aplikasi yang lebih besar, pertimbangkan untuk memisahkan nilai konteks menjadi dua konteks yang berbeda: satu untuk state dan satu lagi untuk action (fungsi dispatch). Ini dapat meningkatkan organisasi kode dan kemudahan pengujian (testability).

Contoh: Daftar Todo dengan Konteks State dan Action yang Terpisah

// 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;
  }
};
// Usage
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;

Pemisahan ini memungkinkan komponen untuk hanya berlangganan ke konteks yang mereka butuhkan, mengurangi render ulang yang tidak perlu. Ini juga memudahkan pengujian unit (unit test) pada reducer dan setiap komponen secara terpisah. Selain itu, urutan pembungkusan provider itu penting. ActionProvider harus membungkus StateProvider.

Praktik Terbaik dan Pertimbangan

Kesimpulan

React Context API adalah alat yang serbaguna untuk manajemen state. Dengan memahami dan menerapkan pola-pola lanjutan ini, Anda dapat secara efektif mengelola state yang kompleks, mengoptimalkan performa, dan membangun aplikasi React yang lebih mudah dipelihara dan skalabel. Ingatlah untuk memilih pola yang tepat untuk kebutuhan spesifik Anda dan mempertimbangkan dengan cermat implikasi performa dari penggunaan konteks Anda.

Seiring berkembangnya React, begitu pula praktik terbaik seputar Context API. Tetap terinformasi tentang teknik dan pustaka baru akan memastikan Anda siap untuk menangani tantangan manajemen state dalam pengembangan web modern. Pertimbangkan untuk menjelajahi pola-pola yang sedang berkembang seperti menggunakan konteks dengan signal untuk reaktivitas yang lebih terperinci.