A basic tutorial on how to make asynchonous API calls with React and Redux.
By Steve Carey - 8/22/2018
I really struggled getting a full understanding of using React and Redux with an API. All the examples I found were too complicated to know what is going on. Personally I like to start with the most basic example possible that still covers the main points. Once I understand that, then I can learn the other things by adding them on one at a time. Oddly enough I couldn't find a single tutorial that does that, so I made one. And here it is. The world's simplest Redux with APIs example.
This app will do a GET request to an API and display the results. Simple as that. In the Advanced section of the official Redux documentation (redux.js.org/advanced) they show you how to build an asychronous application that requests Reddit posts on the topic of "reactjs" or "frontend." So we'll do that, only just for "reactjs." And if you've done the other React with Rails tutorials where we built a simple Rails API, you can use that too.
This tutorial assumes only basic knowledge of Redux (you can do it even with no knowledge of Redux). It does assume you are reasonably familiar with React, ES6 syntax, and the concepts involved with API calls/asynchronous HTTP requests. For reference, React's docs show how to do an AJAX API call without Redux: reactjs.org/docs/faq-ajax.html. That example uses fetch (technically not AJAX but does the same thing) which we'll use in our second example. And it includes a "Loading..." spinner which we will exclude in our Redux examples.
Not that you need it with such a basic tutorial, but the finished code with both examples from this tutorial is on Github.
Redux is a light weight Node package that allows you to keep the current state of your application in one central object called store. Redux can be used on it's own without React. It is useful when React apps get large and start to become unweildy. Docs: redux.js.org/
The key concepts of Redux are the store, actions and reducers. Plus the React-Redux package's connect method to hook it up to React. Below is an brief overview of each concept and it's main methods. If you are new to Redux, just reading them won't provide much understanding. Rather, we'll build the app using all these concepts, then analyze the code line by line.
const store = createStore(reducer)
dispatch(action)
.dispatch(action)
method, you can create a separate action creation function.const functionName = (data) => ({ type: 'TYPE_NAME', payload: data})
dispatch(functionName(data))
connect(mapStateToProps, mapDispatchToProps)(ComponentName);
<Provider store={store}><MainComponent /></Provider>
Use Create React App to set up a React app.
create-react-app redux-apis-simplest-tutorial
cd redux-apis-simplest-tutorial
Install the redux, react-redux, redux-thunk and axios packages:
npm install --save redux react-redux redux-thunk axios
or yarn add redux react-redux redux-thunk axios
Run the server to make sure it's working:
npm start
or yarn start
You should see the default Welcome to React app in your browser.
First let's set up the file structure. We will just add a reducers.js file and remove some of the files we won't use.
touch src/reducers.js rm src/logo.svg rm src/App.test.js rm src/registerServiceWorker.js
That leaves us with three files for our code: index.js, App.js, and reducers.js, plus the css files. Go ahead and populate them first, then we'll break down the code.
src/index.jsimport React from 'react'; import ReactDOM from 'react-dom'; import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; import rootReducer from './reducers'; import { Provider } from 'react-redux'; import './index.css'; import App from './App'; const store = createStore(rootReducer, applyMiddleware(thunk)); ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') );src/App.js
import React, { Component } from 'react'; import { connect } from 'react-redux'; import axios from 'axios'; class App extends Component { componentDidMount() { this.props.getPosts(); } render() { return ( <div> <h1>Reddit ReactJS Posts</h1> <ul> {this.props.posts.map((post, index) => <li key={index}>{post.title}</li> )} </ul> </div> ); } } const receivePosts = (json) => ({ type: 'RECEIVE_POSTS', posts: json.data.children.map(child => child.data), }); const getPosts = () => { return (dispatch) => { return axios.get('https://www.reddit.com/r/reactjs.json') .then(response => { console.log("response.data", response.data); dispatch(receivePosts(response.data)); }) .catch(error => { throw(error); }); }; }; const mapStateToProps = (state) => ({ posts: state.posts }); const mapDispatchToProps = { getPosts }; export default connect(mapStateToProps, mapDispatchToProps)(App);src/reducer.js
import { combineReducers } from 'redux'; const posts = (state = [], action) => { console.log("5.Reducer:action", action); switch (action.type) { case 'RECEIVE_POSTS': return action.posts; default: return state; } }; export default combineReducers({ posts });
Now take a look in your browser. You should see list of Reddit article titles. So now we know it works.
Now let's go over the code line by line, focusing mainly on what is different from using React without Redux.
import React from 'react'; import ReactDOM from 'react-dom'; import { createStore, applyMiddleware } from 'redux'; //1a import thunk from 'redux-thunk'; //1b import { Provider } from 'react-redux'; //1c import rootReducer from './reducers'; //1d import './stylesheets/index.css'; import App from './components/App'; const store = createStore(rootReducer, applyMiddleware(thunk)); //2 ReactDOM.render( <Provider store={store}> //3 <App /> </Provider>, document.getElementById('root') );
1a-d) These are Redux related imports.
1a) CreateStore is a built-in Redux method that creates a Redux store object. ApplyMiddleware is another built-in Redux method that allows you to add middleware packages like Redux-Thunk.
1d) Import the reducer function. We're calling it rootReducer locally for this file, but any name will work.
2) Redux's createStore method creates the store object which will contain the current state of the application. It takes the reducer function as the first argument. CreateStore takes an optional argument for a store enhancer such as the applyMiddleware function that ships with Redux. Place the the middleware we are applying, Thunk, as the argument.
3) Provider is a component from the React-Redux package. It makes the store available to all container components in the application without passing it explicitly. You only need to use it once when you render the root component.
For a small app like this it's okay to put the actions in with the component, but in a larger app you would want to put them into an action file. Also, Redux recommends splitting your components into two files, a presentational component that contains your JSX, and a container component that interacts with Redux. We are putting all these in our App.js file, but we'll use these groupings to break down the code.
This is the component code that interacts directly with Redux using methods from the React-Redux package. If you are changing the state in your views then in the container code of your component you need use React-Redux's connect() function. To use connect() you need to define mapStateToProps or mapDispatchToProps or both.
const mapStateToProps = (state) => ({ posts: state.posts }); //1 const mapDispatchToProps = { getPosts }; //2 export default connect(mapStateToProps, mapDispatchToProps)(App); //3
1) The mapStateToProps function is used with React-Redux's connect method. It is not required, but if we define it, then it will automatically be called any time the store is updated, or in Redux speak it will subscribe to Redux store updates. The method returns a plain object which will be merged into the component's props. It takes the store's state as the first (and in this case only) parameter and updates the "posts" property from the store. Now any time the React component calls this.props.posts, it will have the most recent version from the Redux store object.
Note, for our functions we are generally using function expressions using ES6 arrow function syntax, but function declarations would also work.
2) MapDispatchToProps is also used with React-Redux's connect method, and also not required. It can either be a function or an object. If an object is passed, as it is here, each function inside it (i.e., our getPosts function) is assumed to be a Redux action creator and will be merged with the component's props.
3) The connect method connects the React component (i.e., App) to our Redux store. It takes in the special "connect" methods/objects we defined above as its arguments. We are also exporting the result.
class App extends Component { //1 componentDidMount() { //2 this.props.getPosts(); //2a } render() { //3 return ( <div> <h1>Reddit ReactJS Posts</h1> <ul> {this.props.posts.map((post, index) => //3a <li key={index}>{post.title}</li> //3b )} </ul> </div> ); } }
1) Define a React class component.
1b) There is no 1b. In a React class component without Redux you would set the initial state in a constructor function. But since Redux manages state in the store object we don't need one.
Note on the order of 2 and 3. The render method will execute first. Then componentDidMount. Then after the API call is requested and received, the render method will execute again. So render() executes both before componentDidMount() and after.
2) The React componentDidMount function is invoked immediately after the component is mounted (inserted into the DOM tree).
2a) This is where we make the API call, or rather where we call the getPosts() function that makes the API call.
3) React's render method is the only required method in a class component. It returns the JSX which will be converted to HTML. You can access this.props and this.state in expressions and render will include the value(s) with the JSX.
3a) We need to iterate over the Posts array (when we get it) using React's Lists and Keys process. So we add an expression that looks in this.props for the posts array. If it's there we use the JavaScript map function to iterate over the array.
3b) React requires you to set a key based on a unique attribute. If each item in the list has an id attribute, that would be best. Here we are just using the array index number built into JavaScript arrays.
const receivePosts = (json) => ({ //2 type: 'RECEIVE_POSTS', //2a posts: json.data.children.map(child => child.data), //2b }); const getPosts = () => { //1 return (dispatch) => { //1a return axios.get('https://www.reddit.com/r/reactjs.json') //1b .then(response => { //1c dispatch(receivePosts(response.data)); //1d }) .catch(error => { throw(error); }); }; };
1) We added the getPosts method to the App component's props using mapDispatchToProps with the connect function. And we called getPosts() from the App component's componentDidMount method. Our getPosts method is where we make our API call. It uses ES6 Promises, the Axios package for AJAX, and a Redux action creator.
1a) You need to wrap the action creator into a dispatch call so it can be invoked directly.
1b) Send a get request to the URL. We are using the Axios package to handle the AJAX request behind the scenes.
1c) ES6 Promises will wait for the response before executing. It takes the response as the argument.
1d) When the response is received we need to add it to the store. Only an action object can change the store. Use the dispatch method dispatch(action)
to send the action to the reducer method, which will apply it to the store.
The argument for the dispatch method is the action. Instead of defining the action directly in the argument, we are calling a Redux action creation function, defined above, passing the Response data as the argument. It returns the action. This is how Redux recommends doing it, although we could also just define the action directly in the dispatch method: dispatch({type: 'RECEIVE_POSTS', posts: json.data.children.map(child => child.data)})
.
2) Define the action creation function. This function just returns a Redux action object. It takes the data from the API call as the parameter. We're calling it "json" but you can use any name for the parameter.
2a) An action object must contain a "type" property. The value can be whatever name you find useful. Redux recommends using a string constant for the type's value, but you can also use a string literal like we are here. When the action is evaluated by the reducer function, you will generally use the "type" in an if or switch statement.
2b) If you need to send data to the store you can add a payload property of whatever name is useful, like "posts." When this method was called, the response.data from the API call was sent as the argument. To see what the response would look like in JSON format just paste https://www.reddit.com/r/reactjs.json
into your browser. It's in a bit of a funky object within object format so we'll use the JavaScript map method to change it to a simple array of objects.
Now the action object is sent to the Redux store. The Reducer function takes it from there.
import { combineReducers } from 'redux'; const posts = (state = [], action) => { //1 switch (action.type) { //1a case 'RECEIVE_POSTS': //1b return action.posts; default: //1c return state; } }; export default combineReducers({ posts }); //2
Reducer functions change the value of the store based on the action sent to the store. In this case the RECEIVED_POSTS action sent the posts array to the store but it is the Reducer that applies it to the store.
1) Reducer functions are defined by the programmer and can be whatever name you want. Redux knows it is your reducer because you listed it as the first argument in createStore(reducer) in the src/index.js file. A reducer takes two arguments, the state (meaning the store's current state) and an action. The reducer needs to define an initial state. We are using the ES6 default state syntax to set the initial state to an empty array state = []
.
1a) The reducer uses a switch statement (or if statement) to determine what to return to the store. Remember the action object's "type" property is required. We are using it in the switch statement.
1b) We sent the 'RECEIVE_POSTS' action, so in this case we will return the posts array that was sent as the argument. Redux will add the return value to the store.
1c) The reducer must have a default value. If there are no matches to the action type then we just return the current state back to the store.
2) While we only have one reducer, in most apps you will have more than one. Since there is only one Redux store object, the Redux combineReducers method will combine your reducers in an object when interacting with the store.
When the store is updated that will trigger the mapStateStateToProps function, which in turn will trigger the render method which will display the posts to the User Interface.
You are done! But, I find it helps to redo it with some variations. If you're up for it, on to the next section.
Let's rebuild the app with some changes. In this version we will:
Let's rebuild the app pointing to a different API. If you did the React with Rails tutorial series, or at least the Building the Rails API tutorial, then you have a simple Articles API built with Ruby on Rails that you can run locally on port 3001. Or build it with another framework like Node with Express and MongoDB or Python Django or whatever. You just need an endpoint at localhost:3001/articles that will return an array of JSON article objects with fields for id, title, and content. You could also just redo the Reddit api with these changes if you don't want to build your own API.
Here is the code. You can either start a new app, or just replace the code in the app you created above. The differences are in bold. And this time the explanation will be on the flow following the console.log statements rather than the line by line details of the code.
The first step is to add the redux-logger package
npm install --save redux-logger
or yarn add redux-logger
import React from 'react'; import ReactDOM from 'react-dom'; import { createStore, applyMiddleware} from 'redux'; import thunk from 'redux-thunk'; import rootReducer from './reducers'; import { Provider } from 'react-redux'; import './index.css'; import App from './App'; import logger from 'redux-logger' const store = createStore(rootReducer, applyMiddleware(thunk, logger)); ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') );
Import Redux-Logger and add it as an argument to the applyMiddleware method.
import React, { Component } from 'react'; import { connect } from 'react-redux'; // AppComponent.js class App extends Component { componentDidMount() { console.log('3.this.props', this.props); this.props.getArticles(); } render() { console.log("2.this.props.articles", this.props.articles); return ( <div> <h1>Articles</h1> <ul> {this.props.articles.map((article) => ( <div key={article.id}> <hr /> <h4>{article.id}: {article.title}</h4> <p>{article.content}</p> </div> ))} </ul> </div> ); } } // actions.js export const RECEIVE_ARTICLES = 'RECEIVE_ARTICLES'; const receiveArticles = (data) => ({ type: RECEIVE_ARTICLES, articles: data, }); const getArticles = () => { return (dispatch) => { return fetch('http://localhost:3001/articles.json') .then(response => response.json()) .then(data => { console.log("4.data", data); dispatch(receiveArticles(data)); }) .catch(error => { throw(error); }); }; }; // App.js const mapStateToProps = (state) => { console.log("1.mapStateToProps:state", state); return { articles: state.articles }; }; const mapDispatchToProps = { getArticles }; export default connect(mapStateToProps, mapDispatchToProps)(App);
In the getArticles function, we are using the native JavaScript Fetch API to make our API call instead of using AJAX through the Axios package. This adds one more step of converting the response from a JSON string to JavaScript with the json() method. That step is done behind the scenes when using Axios.
The Fetch API for asynchronous HTTP requests is an alternative to, and potentially replacement for AJAX. It is relatively new and only supported by the four major browsers since March 2017. But like all ES6 features, React will transpire it to ES5 using Babel.
ReceiveArticles function is an action creation function. In our original example we set the type property's value to a string literal (in quotes) like this. type: 'RECEIVE_POSTS'
. Redux recommends using a string constant for type values. So above it we declared a constant set to the string value and export it: export const RECEIVE_ARTICLES = 'RECEIVE_ARTICLES';
. And we set the type to the constant type: RECEIVE_ARTICLES
(without the quotes).
import { combineReducers } from 'redux'; import { RECEIVE_ARTICLES } from './App'; const articles = (state = [], action) => { switch (action.type) { case RECEIVE_ARTICLES: console.log("5.Reducer:action", action); return action.articles; default: return state; } }; export default combineReducers({ articles });
Since we set the Action type to a constant in App.js and exported it, we are importing it in the Reducer. And in the case statement we use the constant (without quotes) instead of the string.
To see it in action make sure you go to the API's root directory and start the server on port 3001: rails server -p 3001
.
You may also need to restart the React app's server: npm start
or yarn start
Then you should see the articles list in your browser. But this time we added the Redux Logger middleware and we added a bunch of console.logs in the code. So in the browser open up the console and refresh the page. This will show the progression of the app:
Okay, we are done. If you want to have more fun, you can add a REQUEST_ARTICLES action when the API request is made from the getArticles function, that displays a "Loading..." spinner until the API data has been received. Or go to the Redux CRUD App Tutorial where we'll not only do an Articles API call, but we'll create, update and delete articles from our API as well. You don't want to miss that.