Thunk, unpacked.
The term “thunk” exists beyond any package you install into your redux application.
You can see here I had to look up the term “subroutine”, as well! “Subroutine” refers to a sequence of programming instructions that perform a very specific task, packaged as a unit. We often call these “functions,” but imagine they’re just one small part of the factory that is your application. If you’re building elevators, for example, simply punching out buttons is a “subroutine”. So you can think of a “thunk” as a grass roots function that works inside of another function, allowing the logic to await execution, much like a callback function.
Rephrased: a “thunk” is a special function returned by another function.
function outside_func() {
return function my_thunk() {
console.log("time to execute!")
}
}// when you're ready to execute call:outside_func()()
So what about “thunk” without the article? I’m talking now about the package you install in your redux application to assist you with any actions that will be making asynchronous calls.
npm install redux-thunk --save
You will now be able to not only return objects from your actions, but also functions!
Imagine a bar finder map app. Here is how our base app could look:
// index.jsimport React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import {createStore, applyMiddleware} from 'redux'
import thunk from 'redux-thunk'
import {Provider} from 'react-redux'
import barReducer from './reducers/barReducer'
import {BrowserRouter as Router} from 'react-router-dom'const store = createStore(barReducer, applyMiddleware(thunk));ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
We are creating a store that will serve as a global state for our app. The first argument to “createStore” is your reducer which you may already be familiar with if you’ve been using redux. But “applyMiddleware” allows us to extend redux with custom functionality, in this case, the thunk package! We wrap our app in redux’s “Provider” and pass in that store as a prop that all of our components will now be able to access by mapping state or dispatch to props.
HOWEVER! If you want to use the WILDLY helpful redux dev tools you will need to utilize compose:
// index.jsimport React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import {createStore, applyMiddleware, compose} from 'redux'
import thunk from 'redux-thunk'
import {Provider} from 'react-redux'
import barReducer from './reducers/barReducer'
import {BrowserRouter as Router} from 'react-router-dom'const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;const store = createStore(barReducer, composeEnhancers(applyMiddleware(thunk)))ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
I can’t imagine building out a program and not utilizing those dev tools, so if this is new to you, check it out: Chrome Dev Tools.
When installed, imported in your component, and applied as middleware, thunk will intercept all dispatched actions, running it through just these 14 lines of code:
// https://github.com/reduxjs/redux-thunk/blob/master/src/index.jsfunction createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => (next) => (action) => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
} return next(action);
};
}const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
This code is checking to see if the action returns an object or a function. If it is a function, it calls that function (or “thunk”!) and returns whatever that function returns.
Imagine this component from which will be making the call:
// containers/BarsContainer.jsimport React from 'react'
import {connect} from 'react-redux'
import {fetchBars} from '../actions/fetchBars'
import Map from '../components/Map';class BarsContainer extends React.Component {
componentDidMount(){
this.props.fetchBars()
}render(){
return(
<Map bars={this.props.bars}/>}/>
)
}
}const mapStateToProps = state => {
return {
bars: state.bars,
}
}export default connect(mapStateToProps, {fetchBars})(BarsContainer);
Our component is connected to our store through connect. Initially, state.bars will house an empty array, allowing our page to load without an error prior to our fetch request’s completion. When the component is mounted, we will then dispatch our fetch request action.
BEFORE
So, let’s say we don’t know about “thunk” or how it works and we try to return an OBJECT when we dispatched fetchBars():
Our Action:
// actions/fetchBars.jsexport function fetchBars() {
const bars = fetch('http://localhost:3000/bars')
.then(response => response.json())
return {
type: 'FETCH_BARS',
payload: bars
};
}
Our action will dispatch before the fetch request has resolved and we’ll get an error.
AFTER
Here’s how we do it utilizing thunk:
// actions/fetchBars.jsexport function fetchBars(){
return (dispatch) => {
fetch('http://localhost:3000/bars')
.then(r => r.json())
.then(bars => dispatch({
type: "FETCH_BARS",
payload: bars
})
)
}
}
We are returning a FUNCTION, which our middleware intercepts, passing in dispatch as an argument. We are now dispatching the action from INSIDE the function. We don’t even hit the dispatch until our promises have resolved and the response has been received. Thus, allowing us to make asynchronous requests! Now we are sending our bars array to our reducer to update state:
// reducers/barReducer.jsexport default function barReducer(state = {bars: []}, action) {
switch(action.type){
case "FETCH_BARS":
return {...state, bars: action.payload}
default:
return state
}
}
Check it out in the dev tools:
Drink and thunk responsibly!