Tutorial: Building a web app with React, Redux, and the Steem Javascript API, PART 3

in #utopian-io6 years ago

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:


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 the reducer 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

Sort:  

I thank you for your contribution. I await your new contributions as you advance through the curriculum.


Your contribution has been evaluated according to Utopian policies and guidelines, as well as a predefined set of questions pertaining to the category.

To view those questions and the relevant answers related to your post, click here.


Need help? Write a ticket on https://support.utopian.io/.
Chat with us on Discord.
[utopian-moderator]

You got a 3.24% upvote from @postpromoter courtesy of @nicknyr!

Want to promote your posts too? Check out the Steem Bot Tracker website for more info. If you would like to support the development of @postpromoter and the bot tracker please vote for @yabapmatt for witness!

Nice post keeep it up ... upvoted.
upvote me back thanks!

Hey @nicknyr
Thanks for contributing on Utopian.
We’re already looking forward to your next contribution!

Want to chat? Join us on Discord https://discord.gg/h52nFrV.

Vote for Utopian Witness!

Coin Marketplace

STEEM 0.28
TRX 0.13
JST 0.032
BTC 61143.11
ETH 2928.78
USDT 1.00
SBD 3.56