Moderní tvorba webových aplikací
O webu

Thunky v JavaScriptu

Co je thunk, jak funguje a k čemu se používá. Lazy loading, dependency injection, trampolining, testování a Redux Thunk.

10 minut

Thunk je funkce, která obaluje výraz a odkládá jeho vyhodnocení. Místo okamžitého výpočtu vrátíte funkci, která výpočet provede až při zavolání.

Co je thunk

Slovo „thunk” vzniklo v 60. letech při vývoji ALGOL 60. Programátoři potřebovali pojmenovat kus kódu, který se vyhodnotí později — a vtipně použili neformální minulý čas slovesa ”think": „it has already been thunk” (místo správného ”thought"). V praxi jde o wrapper funkci:

// Přímá hodnota
const value = 1 + 2;

// Thunk - odložený výpočet
const thunk = () => 1 + 2;

// Hodnota se vypočítá až teď
console.log(thunk()); // 3

Thunk je tedy funkce, která:

  • Obaluje výpočet nebo operaci
  • Odkládá vyhodnocení až do momentu zavolání
  • Typicky nepřijímá argumenty (ale existují varianty)

Synchronní thunk

Nejjednodušší forma thunku odkládá synchronní výpočet:

function createThunk(x, y) {
  return function() {
    return x + y;
  };
}

const thunk = createThunk(10, 20);

// Výpočet ještě neproběhl
console.log(typeof thunk); // "function"

// Teď se výpočet provede
console.log(thunk()); // 30

Synchronní thunky se hodí pro lazy evaluation — výpočet se provede jen když je potřeba:

function expensiveCalculation() {
  console.log("Počítám...");
  return Array(1000000).fill(0).reduce((a, b) => a + 1, 0);
}

// Vytvoříme thunk místo přímého volání
const lazyResult = () => expensiveCalculation();

// Výpočet se provede až při skutečné potřebě
if (needsResult) {
  console.log(lazyResult());
}

Asynchronní thunk

Thunk může obalovat i asynchronní operaci. Místo hodnoty pak vrací Promise nebo přijímá callback:

// Thunk s callbackem
function fetchUserThunk(userId) {
  return function(callback) {
    fetch(`/api/users/${userId}`)
      .then(response => response.json())
      .then(data => callback(null, data))
      .catch(error => callback(error));
  };
}

const getUser = fetchUserThunk(42);
getUser((error, user) => {
  if (error) return console.error(error);
  console.log(user);
});

Modernější verse s Promise:

// Thunk vracející Promise
function fetchUserThunk(userId) {
  return function() {
    return fetch(`/api/users/${userId}`)
      .then(response => response.json());
  };
}

const getUser = fetchUserThunk(42);
getUser().then(user => console.log(user));

Thunk s memoisací

Thunk lze rozšířit o memoisaci — výpočet proběhne jen jednou a výsledek se uloží:

function memoisedThunk(fn) {
  let cached = false;
  let result;

  return function() {
    if (!cached) {
      result = fn();
      cached = true;
    }
    return result;
  };
}

const expensiveThunk = memoisedThunk(() => {
  console.log("Počítám pouze jednou");
  return Math.random();
});

console.log(expensiveThunk()); // Počítám pouze jednou, 0.123...
console.log(expensiveThunk()); // 0.123... (bez výpisu)
console.log(expensiveThunk()); // 0.123... (bez výpisu)

Redux Thunk

Nejznámější využití thunků v JavaScriptu je Redux Thunk middleware. Umožňuje dispatchovat funkce místo plain objektů:

// Běžná akce - plain objekt
const setUser = (user) => ({
  type: 'SET_USER',
  payload: user
});

// Thunk akce - funkce
const fetchUser = (userId) => {
  return async (dispatch, getState) => {
    dispatch({ type: 'FETCH_USER_START' });

    try {
      const response = await fetch(`/api/users/${userId}`);
      const user = await response.json();
      dispatch(setUser(user));
    } catch (error) {
      dispatch({ type: 'FETCH_USER_ERROR', error });
    }
  };
};

// Použití
store.dispatch(fetchUser(42));

Redux Thunk middleware detekuje, že akce je funkce, a zavolá ji s dispatchgetState jako argumenty. To umožňuje:

  • Asynchronní operace (API volání)
  • Podmíněné dispatchování
  • Přístup k aktuálnímu stavu
  • Dispatchování více akcí

Implementace middleware

Redux Thunk middleware je překvapivě jednoduchý:

const thunkMiddleware = ({ dispatch, getState }) => {
  return (next) => (action) => {
    if (typeof action === 'function') {
      return action(dispatch, getState);
    }
    return next(action);
  };
};

Celá logika: pokud je akce funkce, zavolej ji. Jinak předej dál.

Thunk vs. Promise

Thunky a Promise řeší podobný problém — representaci budoucí hodnoty. Hlavní rozdíl:

Thunk Promise
Spuštění Při zavolání Okamžitě při vytvoření
Opakované volání Spustí znovu Zachovává výsledek
Lazy evaluation Ano Ne
Zrušení Nezavoláte Nelze (bez AbortController)
// Promise - spustí se hned
const promise = fetch('/api/data');

// Thunk - spustí se až při zavolání
const thunk = () => fetch('/api/data');

// Promise běží, i když výsledek nepotřebujeme
// Thunk se nespustí, dokud ho nezavoláme

Praktické využití

Následující příklady ukazují thunky v kontextu Reduxu. Funkce dispatch odesílá akce do Redux store — je to způsob, jak říct Reduxu „změň stav podle této akce”.

Podmíněné načítání

const loadDataIfNeeded = () => {
  return (dispatch, getState) => {
    const { data, loading } = getState();

    // Nenačítej, pokud už data máme nebo načítáme
    if (data || loading) return;

    dispatch(fetchData());
  };
};

Debounced akce

let timeout;
const debouncedSearch = (query) => {
  return (dispatch) => {
    clearTimeout(timeout);
    timeout = setTimeout(() => {
      dispatch(search(query));
    }, 300);
  };
};

Sekvenční akce

const checkout = () => {
  return async (dispatch) => {
    await dispatch(validateCart());
    await dispatch(processPayment());
    await dispatch(sendConfirmation());
    dispatch(clearCart());
  };
};

Lazy loading modulů

Thunky umožňují načítat moduly, až když jsou potřeba:

// Thunk pro lazy import
const getChart = () => import('chart.js');
const getEditor = () => import('monaco-editor');

// Použití - modul se načte až při volání
async function showChart(data) {
  const { Chart } = await getChart();
  new Chart(canvas, { type: 'line', data });
}

// Editor se nenačte, dokud uživatel neklikne
button.onclick = async () => {
  const monaco = await getEditor();
  monaco.editor.create(container, options);
};

Výhoda oproti přímému importu: moduly se nenačítají při startu aplikace, ale až když jsou skutečně potřeba.

Dependency injection

Thunky lze použít pro jednoduché předávání závislostí. Není to typický přístup k DI (pro komplexní aplikace existují specialisované knihovny jako InversifyJS), ale pro menší projekty může být dostatečný:

// Závislosti jako thunky
const createService = (getLogger, getDatabase) => ({
  async save(data) {
    const logger = getLogger();
    const db = getDatabase();

    logger.info('Saving data...');
    await db.insert(data);
  }
});

// Konfigurace závislostí
const service = createService(
  () => console,               // dev logger (console.info)
  () => new SQLiteDatabase()   // dev database
);

// V produkci
const prodService = createService(
  () => new CloudLogger(),
  () => new PostgresDatabase()
);

Závislosti se vytvoří až při použití, ne při inicialisaci.

Trampolining (odskakování)

Trampolining je technika, která řeší přetečení zásobníku u rekursivních funkcí. Název pochází z analogie s trampolínou — místo zanořování do rekurse se funkce „odráží” zpět a volání probíhá v cyklu:

// Klasická rekurse - může přetéct stack
function factorial(n) {
  if (n <= 1) return 1;
  return n * factorial(n - 1);
}

// Trampolining s thunky
function trampoline(fn) {
  let result = fn();
  while (typeof result === 'function') {
    result = result();
  }
  return result;
}

function factorialThunk(n, acc = 1n) {
  if (n <= 1) return acc;
  return () => factorialThunk(n - 1, BigInt(n) * acc); // Vrací thunk
}

// Bezpečné i pro velká čísla (BigInt)
console.log(trampoline(() => factorialThunk(100))); // 93326215443944152...

Místo rekursivního volání funkce vrací thunk. Trampolína thunky rozbaluje v cyklu, takže nedochází k hromadění na zásobníku.

Odložená konfigurace

Thunky umožňují definovat konfiguraci, která se vyhodnotí až za běhu:

const config = {
  apiUrl: () => process.env.API_URL || 'http://localhost:3000',
  retries: () => parseInt(process.env.RETRIES) || 3,
  features: () => ({
    darkMode: localStorage.getItem('darkMode') === 'true',
    beta: document.cookie.includes('beta=1')
  })
};

// Hodnoty se načtou až při přístupu
async function makeRequest(endpoint) {
  for (let i = 0; i < config.retries(); i++) {
    const response = await fetch(config.apiUrl() + endpoint);
    if (response.ok) return response;
  }
}

Konfigurace může záviset na stavu, který není dostupný při inicialisaci (localStorage, cookies, env proměnné).

Testování a mockování

Thunky usnadňují testování tím, že oddělují vytvoření závislosti od jejího použití:

// Produkční kód
const createUserService = (getApi = () => realApi) => ({
  async getUser(id) {
    const api = getApi();
    return api.fetch(`/users/${id}`);
  }
});

// Test
describe('UserService', () => {
  it('fetches user by id', async () => {
    const mockApi = {
      fetch: jest.fn().mockResolvedValue({ id: 1, name: 'Jan' })
    };

    const service = createUserService(() => mockApi);
    const user = await service.getUser(1);

    expect(mockApi.fetch).toHaveBeenCalledWith('/users/1');
    expect(user.name).toBe('Jan');
  });
});

Event handlery

Thunky oddělují definici handleru od jeho spuštění:

// Factory pro event handlery
const createClickHandler = (action) => {
  return (event) => {
    action(event.target);
  };
};

// Definice handlerů - logika oddělená od DOM
const handlers = {
  'submit-btn': createClickHandler((el) => el.form.submit()),
  'reset-btn': createClickHandler((el) => el.form.reset()),
  'menu-btn': createClickHandler((el) => {
    document.getElementById('menu').classList.toggle('open');
  })
};

// Připojení až když DOM existuje
document.addEventListener('DOMContentLoaded', () => {
  Object.entries(handlers).forEach(([id, handler]) => {
    document.getElementById(id)?.addEventListener('click', handler);
  });
});

Alternativy k Redux Thunk

Pro komplexnější asynchronní logiku existují alternativy:

  • Redux Saga — používá generátory, lepší pro komplexní flow
  • Redux Observable — používá RxJS, reaktivní přístup
  • RTK Query — vestavěné řešení v Redux Toolkit pro data fetching

Pro většinu aplikací je však Redux Thunk dostatečný a jeho jednoduchost je výhodou.

Nevýhody thunků

  • Horší čitelnost — vnořené funkce mohou být matoucí, zejména pro začátečníky
  • Těžší debugování — stack trace nemusí být přehledný, protože thunky jsou anonymní funkce
  • Opakované vyhodnocení — na rozdíl od Promise se thunk při každém volání spustí znovu (pokud není memoisovaný)
  • Implicitní závislosti — thunky v Redux Thunk mají přístup k dispatch a getState, což může vést k těsné vazbě na store
  • Složitější testování — pro testování Redux thunků je potřeba mockovat dispatch a getState

Shrnutí

  • Thunk je funkce obalující výraz pro odložené vyhodnocení
  • Umožňuje lazy evaluation — výpočet proběhne až když je potřeba
  • Na rozdíl od Promise se thunk spustí až při zavolání

Využití thunků:

  • Redux Thunk — asynchronní akce v Reduxu
  • Lazy loading — dynamický import modulů
  • Dependency injection — odložené vytváření závislostí
  • Trampolining — optimalisace rekurse
  • Konfigurace — hodnoty závislé na runtime stavu
  • Testování — snadné mockování závislostí

Novinky e-mailem

Když budu mít něco opravdu zajímavého, můžu vám to poslat e-mailem

Přidej se k 500+ čtenářům
Jen kvalitní obsah
Žádný spam

Web jecas.cz píše Bohumil Jahoda, kontakt
Seznam všech článků · Témata
2013–2026