Why doesn't Redux support AJAX out of the box?
28 Sep 2016
But how the hell do I make remote requests with Redux?
— every newcomer, ever
Wow, you know quite a bit of React already; Redux too. You're at the point where your little project needs to start talking to the outside world.
Now... you feel a bit unsure where would this fit. Should it go into a reducer? or?..
How should it even work???
Recap of Redux
What is Redux, essentially?
It says in the readme
Redux is a predictable state container
and while the claim is true, there's more to it than that.
The cornerstone of Redux is an event log. All kinds of things that happen in the app are recorded as facts.
A typical interaction with a todo app can be represented with events like this:
ADD_TODO { text = "Do groceries" }
ADD_TODO { text = "Call Stan" }
COMPLETE_TODO { text = "Do groceries" }
ADD_TODO { text = "Wash dishes" }
The state is a different view on actions, it helps the application make sense of the past events. Reducers are a piece that updates the state based on actions that have happened.
You know all of that already! I just wanted to emphasize it one more time.
So how it all relates to AJAX, again?
Easy.
Suppose we have an app which has one button called "Random image" When it's clicked, we reach out to the server, get a random image and render it.
Putting some thought into formalizing the task at hand can be helpful. It can feel pointless and time-consuming at first, but it helps you get a clear picture.
Let's try to think what happens there in terms of events.
LOAD_RANDOM_IMAGE
for sure!
LOAD_RANDOM_IMAGE
??? - what happens there?
Is that it? Nope. What else would be a cold hard fact there?
The response we got from the server! It happened, we need it, so it's definitely something we'll want to record.
So need two events, one to represent a button click, another to receive the image (don't stress out much about how it happens yet):
LOAD_RANDOM_IMAGE
RECEIVE_RANDOM_IMAGE { url = "http://example.com/cat-samba.gif" }
Tying it together
Awesome! Now we got our domain events straight.
There's still one piece of the puzzle missing. How do requests happen?
Normally, what goes into mapDispatchToProps
is the dispatching of a single action:
function mapDispatchToProps(dispatch) {
return {
loadRandomImage() {
dispatch(actions.loadRandomImage());
}
};
}
But in this case, we clearly need more. We need to dispatch one action immediately and dispatch another one when we receive the response from the server.
How do we do it? Easy.
function mapDispatchToProps(dispatch) {
return {
loadRandomImage() {
dispatch(actions.loadRandomImage());
api.fetchRandomImage().then(image => {
dispatch(actions.receiveRandomImage(image));
});
}
};
}
Note that we are agnostic of how the API requests happen: with fetch
, $.ajax
, or plain ol' XMLHttpRequest
.
That is abstracted away in an api
module.
All we care about is, it returns a promise.
Now, having that right inside mapDispatchToProps
can feel a bit dirty, let's move that to a separate function.
One that takes dispatch
.
function loadRandomImage(dispatch) {
dispatch(actions.loadRandomImage());
api.fetchRandomImage().then(image => {
dispatch(actions.receiveRandomImage(image));
});
}
function mapDispatchToProps(dispatch) {
return {
loadRandomImage() {
loadRandomImage(dispatch);
}
};
}
Where does this function belong? Wherever you see fit. A couple of places it's usually placed:
- next to
mapDispatchToProps
- in the action creators module
- in a separate file
Whichever works for you is fine, really. Don't buy on the "it HAS to be in X," try all of those and see which works better for you.
Back to the original question
— So why doesn't Redux ship with AJAX support?
— Because AJAX is not a weird cousin that need special support. AJAX fits in naturally into the Redux model.
Bonus: thunks
You may have noticed that with regular actions, it's usually dispatch(action())
, but what are doing is action(dispatch)
.
There is nothing with this approach — after all, we need to dispatch several actions, not one, so it makes sense.
However, if this feels a bit out-of-place for your taste, you can try Redux-Thunk, which allows you to go back to dispatch(action())
, by making dispatch
accept a function.
In our case, after applying redux-thunk
, mapDispatchToProps
could be this:
function mapDispatchToProps(dispatch) {
return {
loadRandomImage() {
dispatch(loadRandomImage);
}
};
}
This way the components don't care whether an action is smart or not.
I've also written about two Redux anti-patterns: