
Thunky v JavaScriptu
Co je thunk, jak funguje a k čemu se používá. Lazy loading, dependency injection, trampolining, testování a Redux Thunk.
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 dispatch a getState 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í