https://github.com/facebook/react
Building a web app with React, Redux, and the Steem Javascript API, Part 3
Image from mit.edu
In parts 1 and 2 of this tutorial we learned how to install and use both the dsteem
and steem.js
APIs, install and set up node-sass-chokidar
to use Sass in our project, and how to work with the very messy JSON data returned by the APIs and get the data to display in a React environment.
Iterating through the data proved to be difficult and unsightly. In this tutorial I will teach you how to use the json-query package to simplify iterating through deeply nested objects. We will also be migrating away from using local component state and migrate to using Redux and get up and running with it. As our project grows and becomes more intricate and starts sending out more API requests Redux will prove to be very useful.
What you will learn in this tutorial:
- Installing and using json-query to simplify working with nested JSON data
- Installing and getting up and running with Redux to manage state
- The basics of Redux actions, reducers and store
Requirements:
- All the requirements from part 2 of this series
- json-query
- Redux
Difficulty: Intermediate
What is Redux and why is it important?
Before we get started with migrating to Redux let's talk about what Redux is and why it is useful. Redux is a library that allows developers to manage state outside of React components in one big store rather than managing state locally within React components. Or, more eloquently put:
Redux is a predictable state container for JavaScript apps.
(Not to be confused with a WordPress framework – Redux Framework.)
It helps you write applications that behave consistently, run in different environments (client, server, and native), and are easy to test. On top of that, it provides a great developer experience, such as live code editing combined with a time traveling debugger.
You can use Redux together with React, or with any other view library.
It is tiny (2kB, including dependencies).
Source : Redux documentation
In other words, Redux can be paired with React to help us manage our state in a more consistent and easier to reason about way. Instead of using local state in React components via this.state
at the top of each component we can store our state outside the component and have one store that holds the state for every component in our project. Many components can be created and store data from many sources and all of it will be stored in the same Redux store.
The core concepts of Redux:
- Single source of truth: The state of your whole application is stored in an object tree within a single store.
- State is read-only: The only way to change the state is to emit an action, an object describing what happened.
- Changes are made with pure functions: To specify how the state tree is transformed by actions, you write pure reducers.
Taken straight from the documentation
These three concepts are the basic principles behind Redux. Familiarize yourself by reading the the full documentation.
Now that we're familar with what Redux can do for us and what the core ideas behind Redux are let's start talking about the various parts and how to put them together to get started with using Redux in the project.
Getting started with Redux
First we need to install the appropriate Redux packages:
npm i redux
npm install --save react-redux
npm install --save-dev redux-devtools
npm i redux-thunk
npm i redux-logger
And, while we're at it, we might as well install json-query
for later use:
npm i json-query
After these packages are installed the next step is to set up our project folder structure so that it is appropriate for working with Redux. The first step is to create individual folders for our actions, reducers, and components. I'll get to what actions and reducers do later. For now just make sure your project's folder structure looks like this:
Basic folder structure for Redux setup
Note that we have a folder named actions
which contains a Utopian-action
action file, a folder named reducers
which contains a Utopian-reducer
reducer file, and that our App.js
file and our Utopian.js
components (along with any other components we'll add) are in a component
folder.
Now that we have the basic file structure set up let's talk actions, reducers, and store.
Redux actions
In simple terms actions are basic Javascript objects that send data from your project to the Redux store. Actions must contain a "type" property which is little more than a string that describes the action itself. Actions are sent to the Redux store via using store.dispatch()
.
In our project we will be using action creators, which are basically just functions that return an action. The Redux documentation shows us an example of a basic action creator that illustrates the difference between the action creator and the action itself:
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
The function addTodo is the action creator, the returned object is the action itself
An important thing to understand is that actions/action creators are synchronous in nature. In order to send asynchronous requests (like the API request we want to send) we will have to utilize Redux middleware, in particular redux-thunk
.
Using Redux-thunk to make our actions asynchronous:
Redux-thunk
is a package that allows us to create actions that are asynchronous in nature such as API calls. With regular actions only an object can be returned, but with Thunk we can create actions that return a function, thus allowing us to use promises and callbacks to send asynchronous requests.
Our Utopian-action
file uses Thunk to send the API requests and looks like this:
import dsteem from 'dsteem';
import { Client } from 'dsteem';
export function fetchUtopian() {
return function (dispatch) {
dispatch({ type: "FETCH_UTOPIAN" });
// API variable created
const client = new Client('https://api.steemit.com')
// API query
var utopianHot = {
tag: 'utopian-io',
limit: 20
}
// Actual API call
client.database
.getDiscussions('hot', utopianHot)
// Thunk allows us to return functions and handle API response in promises/.then
.then((response) => {
// Notice here that Thunk allows us to return a function, instead of just an object
dispatch({ type: "FETCH_UTOPIAN_FULFILLED", payload: response })
})
.catch((err) => {
dispatch({ type: "FETCH_UTOPIAN_REJECTED", payload: err })
})
}
}
The above code imports the API, creates the API variable and query the same way we did in part 2 of this series using local state, and creates three dispatch
functions. FETCH_UTOPIAN
to send out the request, FETCH_UTOPIAN_FULFILLED
which handles what to do with the data/how to dispatch it to the store in the event the API request is successful, and FETCH_UTOPIAN_REJECTED
which handles how to dispatch the data in the event the API request is unsuccessful.
Without using Thunk we wouldn't be able to return functions in Utopian-action
and thus wouldn't be able to send asynchronous API requests.
Now that we know how to create actions (the "what") let's talk about using reducers to handle the "how".
Redux reducers
Reducers control how the state of our store is changed after actions are sent to the store. Reducers need to be pure functions and should never mutate data or contain things that are asynchronous such as API calls. Reducers take the previous state of the store and an action, and return the new state of the store.
The first part of our Utopian-reducer
file looks like this:
export default function reducer (state={
utopianCash: [],
fetching: false,
fetched: false,
error: null,
}, action) {
}
The first part of our reducer
Note that the function has two parameters: state and action. State contains the utopianCash
array that will store the API data retrieved, and statuses for fetching
, fetched
, and error
.
The next part of our reducer contains a switch statement that handles how to update the state depending on the three scenarios defined in our Utopian-action
action file. How to update state when FETCH_UTOPIAN
is in the process of fetching
, how to update state when FETCH_UTOPIAN_REJECTED
occurs in the event of an error, and how to update state when FETCH_UTOPIAN_FULFILLED
occurs in the event the API request is successful. After the switch statements takes the appropriate path it returns state
:
switch (action.type) {
case "FETCH_UTOPIAN": {
return {...state, fetching: true}
}
case "FETCH_UTOPIAN_REJECTED": {
return {...state, fetching: false, error: action.payload}
}
case "FETCH_UTOPIAN_FULFILLED": {
return {
...state,
fetching: false,
fetched: true,
utopianCash: action.payload
}
}
}
return state;
The switch statement in our reducer that handles what to do depending on the results of the action
All in all our reducer file, Utopian-reducer
looks like this:
export default function reducer (state={
utopianCash: [],
fetching: false,
fetched: false,
error: null,
}, action) {
switch (action.type) {
case "FETCH_UTOPIAN": {
return {...state, fetching: true}
}
case "FETCH_UTOPIAN_REJECTED": {
return {...state, fetching: false, error: action.payload}
}
case "FETCH_UTOPIAN_FULFILLED": {
return {
...state,
fetching: false,
fetched: true,
utopianCash: action.payload
}
}
}
return state;
}
The full reducer file
There's one more reducer related thing we need to handle. In large applications you might have many reducers all updating the state. In order to import many reducers all together and in the neatest possible way it's a good idea to create an index.js
file within the reducer
folder and combine all the reducers into one function so they can be bundled up and get served to the state once rather than individually.
Here's our combine reducer index.js
file:
// Combine Reducers in this file and export them
import { combineReducers } from 'redux'
import utopianReducer from './Utopian-reducer';
export default combineReducers({
utopianReducer
// Future reducers we create will go here and all of them are served together
})
Combining our reducers in
index.js
within thereducer
folder
Now that we have our actions and both parts of our reducer (the actual reducer and the combine reducer file) we can create our store and update the state of our store with the data retrieved from the API request.
Our Redux store
Redux applications have one store that is immutable and contains all of the state for the entire application. Stores update state via dispatch(action)
.
Our store is located in index.js
within the src
folder, will utilize middleware (redux-thunk
and logger
) and looks like this:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';
import registerServiceWorker from './registerServiceWorker';
import { Provider } from 'react-redux';
import logger from 'redux-logger';
// Allows us to use thunk and logger
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from "redux-thunk";
// Imports our reducers that tell the store how to update
import reducers from './reducers';
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
// Creates store and applies middleware so we can use thunk and logger
const store = createStore(reducers, composeEnhancers(
applyMiddleware(thunk, logger)
));
// Wraps our entire App in Provider and connects store to our application
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root'));
We now have our actions, reducers, combined reducers, and store set up. There is a wealth of information available online pertaining to these concepts and to Redux patterns. For the sake of keeping this tutorial concise and brief I won't go deeper into how Redux works. From here on out I will focus on how we can grab the data stored in Redux state from the Redux store and display it in our Utopian.js
component.
Changing Utopian.js
to migrate to using Redux state
In part 2 of this series we used local state to render the data in our Utopian.js
component. We'll be abandoning this and migrating to using Redux now.
Import the following packages and delete most of the component. Utopian.js
should look like this now:
import React, { Component } from 'react';
import dsteem from 'dsteem';
import { Client } from 'dsteem';
import User from './User';
import jsonQuery from 'json-query';
import { connect } from 'react-redux';
import { fetchUtopian } from '../actions/Utopian-action';
import {bindActionCreators, compose, applyMiddleware, createStore} from 'redux';
import thunk from 'redux-thunk';
class Utopian extends Component {
render() {
return (
<div className="utopian-items">
</div>
)
});
return (
<div className="utopian-container">
{display}
</div>
);
}
}
Local state and the messy way we iterated through the JSON data in part 2 is now removed
In order to access the data in the store we will be using two functions: mapStateToProps
and matchDispatchToProps
.
mapStateToProps
mapStateToProps
allows us to access our states in an easier way via .props
in our component i.e. this.props
mapDispatchToProps
mapDispatchToProps
allows us to access dispatch()
functions in our component i.e. this.props.fetchUtopian
We implement both these functions at the bottom of our Utopian.js
component like so:
const mapDispatchToProps = dispatch => ({
fetchUtopian: () => dispatch(fetchUtopian())
})
const mapStateToProps = state => ({
data: state.utopianReducer
})
Now we are almost ready to start accessing the data from our Redux store in our Utopian.js
component. The last thing we need to do is connect our component to the store via connect
:
export default connect(mapStateToProps, mapDispatchToProps)(Utopian);
Note that we call export default
the way we normally would to pass a component to another component. Then connect
is called and is passed mapStateToProps
and mapDispatchToProps
. After that, we pass the name of the component itself, in this case Utopian
.
All in all our Utopian.js
component, when utilizing Redux state, will look like this:
import React, { Component } from 'react';
import dsteem from 'dsteem';
import { Client } from 'dsteem';
import User from './User';
import jsonQuery from 'json-query';
import { connect } from 'react-redux';
import { fetchUtopian } from '../actions/Utopian-action';
import {bindActionCreators, compose, applyMiddleware, createStore} from 'redux';
import thunk from 'redux-thunk';
class Utopian extends Component {
componentDidMount() {
this.props.fetchUtopian();
}
render() {
console.log(this.props);
return (
<div className="utopian-container">
</div>
);
}
}
const mapDispatchToProps = dispatch => ({
fetchUtopian: () => dispatch(fetchUtopian())
})
const mapStateToProps = state => ({
data: state.utopianReducer
})
export default connect(mapStateToProps, mapDispatchToProps)(Utopian);
Note that we console.log
the data being passed to the component via this.props
. Run npm start
and open the browser dev tools and you'll see the posts data from the API is retrieved.
The Redux state logged out by Redux-logger
The inner contents of the JSON data. Note that it's the same structure as when we used local state in part 2
One final part: Using json-query
to simplify working with the data
This tutorial is getting a bit long but I want to include using json-query
to work with the data because it is important and drastically simplifies accessing the nested objects in the JSON data but I don't think it warrants it's own part 4 of the tutorial.
In the previous tutorial we accessed the name of the author in the JSON data like so:
<p>
<strong>Author:</strong>
{this.state.utopianCash[posts[i]]["author"]}
</p>
With the use of json-query
we can access the nested objects in a neater and simpler way:
var author = jsonQuery('[**][author]', { data: this.props.data.utopianCash }).value
First we set a variable equal to jsonQuery()
. We pass jsonQuery()
an array with an asteriks for each level we want to access within the object. In our case the object we are trying to access is two levels nested with the outer element so we access it via [**]
and the name of the property we want [author]
. The second parameter passed to jsonQuery
is the data we are accessing and the value it contains. In our case it is the data from the store being accessed, and .value
.
Now we can access the elements we want in the render function simply by calling .map
the same way we did in part 2, but now the syntax for displaying the data is much more simple. The following displays the exact same elements we did in part 2 but in a much neater way:
import React, { Component } from 'react';
import dsteem from 'dsteem';
import { Client } from 'dsteem';
import User from './User';
import jsonQuery from 'json-query';
import { connect } from 'react-redux';
import { fetchUtopian } from '../actions/Utopian-action';
import {bindActionCreators, compose, applyMiddleware, createStore} from 'redux';
import thunk from 'redux-thunk';
class Utopian extends Component {
componentDidMount() {
this.props.fetchUtopian();
}
render() {
const utopian = Object.keys(this.props.data.utopianCash);
console.log(this.props);
//console.log(utopian);
var author = jsonQuery('[**][author]', { data: this.props.data.utopianCash }).value
console.log(author);
var title = jsonQuery('[**][title]', { data: this.props.data.utopianCash }).value
var payout = jsonQuery('[*][total_payout_value]', { data: this.props.data.utopianCash }).value
var postLink = jsonQuery('[*][url]', { data: this.props.data.utopianCash }).value
var pendingPayout = jsonQuery('[*][pending_payout_value]', { data: this.props.data.utopianCash }).value
var netVotes = jsonQuery('[*][net_votes]', { data: this.props.data.utopianCash }).value
let display = utopian.map((post, i) => {
return (
<div className="utopian-items">
<p>
<strong>Author:</strong>
{author[i]}
</p>
<p>
<strong>Title:</strong>
<a href={`https://www.steemit.com` + postLink[i]}>{title[i]}</a>
</p>
<p>
<strong>Pending Payout:</strong>
{pendingPayout[i]}
</p>
<p>
<strong>Votes: </strong>
{netVotes[i]}
</p>
</div>
)
});
return (
<div className="utopian-container">
{display}
</div>
);
}
}
const mapDispatchToProps = dispatch => ({
fetchUtopian: () => dispatch(fetchUtopian())
})
const mapStateToProps = state => ({
data: state.utopianReducer
})
export default connect(mapStateToProps, mapDispatchToProps)(Utopian);
The data displayed by Utopian.js
is formatted the same way on the screen except now we are using Redux state to store the data and we are using json-query
to access it in a neater way within the component.
Our
Utopian.js
component
End of part 3
This concludes part 3 of the series. I hope you learned how to get started using Redux and got a grasp of the basics. This part was longer than I initially wanted it to be but I chose to write it as one part because I think it is important to cover Redux in one tutorial rather than splitting it up into multiple parts. In part 4 I will be adding more API calls and we will be making a more robust app that displays more useful data pertaining to top posts from Utopian-io.
Proof of work:
https://github.com/Nicknyr/Steem.js_API_Tutorial