The SoundCloud Client in React + Redux

 by Robin Wieruch
 - Edit this Post

In the beginning of 2016 it was time for me to deep dive into the ReactJs world. I read tons of articles about React and its environment, especially Redux, so far. Several of my colleagues used it in side projects and on a theoretical level I could participate in the discussions.

In my company we relied heavily on Angular 1 at this point. Since we are using it in a quite large code base, we know a lot about its flaws. Back in 2015 we already adopted our own flux architecture in the Angular world with the usage of stores and an unidirectional data flow. We were highly aware of the change coming with the React environment.

Again in the early days of 2016 I wanted to see this hyped paradigm shift in its natural environment (React and its flux successor Redux) with a hands on side project.

It took me some weeks to implement the SoundCloud Client FaveSound. Being both a passionate SoundCloud consumer and producer, it felt compelling for me to do my own SoundCloud client in React + Redux.

Professionally I grew with the code base, but also got an entry point into the open source community by providing a larger code base example for beginners in the React + Redux world. Since I made this great experience, I wanted to give the community this hands on tutorial, which will guide people to get started in React + Redux with a compelling real world application - a SoundCloud client.

At the end of this tutorial you can expect to have a running React + Redux app, which consumes the SoundCloud API (). You will be able to login with your SoundCloud account, list your latest tracks and listen to them within the browser. Additionally you will learn a lot about tooling with Webpack and Babel.

In the future I am going to write some smaller tutorials based on this one. They will simply build on top of this project and help you to get started in various topics. So keep an eye on this tutorial, follow me on Twitter or GitHub or simply star the repository to get updates.

Table of Contents

Extensions

A list of extensions which can be applied on top of the SoundCloud Client with React + Redux tutorial afterwards.

A project from scratch

I must say I learned a lot from implementing a project from scratch. It makes totally sense to set up your side project from zero to one without having a boilerplate project. You will learn tons of stuff not only about React + Redux, but also about JavaScript in general and its environment. This tutorial will be learning by doing by understanding each step, like it was for me when I did this whole project, with some helpful explanations. After you have finished this, you should be able to set up your own React + Redux side project to provide another real world project for the community.

The whole tutorial contains a lot of information. I wouldn't suggest to do everything at once when you are still learning React + Redux. Make some breaks between the chapters. Once you build your first React component, don't continue with Redux immediately. Experiment a bit with the code, do some internal state management with React, before you use Redux for state management. Take your time.

Additionally I can recommend to read The Road to learn React before you dive into Redux. It teaches React by building a Hacker News App without configuration, tooling and Redux. If you are new to React, do yourself a favour and learn React first.

{{% package_box "The Road to React" "Build a Hacker News App along the way. No setup configuration. No tooling. No Redux. Plain React in 200+ pages of learning material. Pay what you want like 50.000+ readers." "Get the Book" "img/page/cover.png" "https://roadtoreact.com/" %}}

Let’s get started

Before you can write your first React component, you have to install Webpack and Babel. I extracted the React setup into an own article to make it reusable and maintainable for the future. You can follow the to setup your project. After that you can come back to this tutorial and continue here to write your first React component.

Is your project set up? Then let's render some data. It makes sense to render a list of tracks, since we are writing a SoundCloud application.

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
const tracks = [
{
title: 'Some track'
},
{
title: 'Some other track'
}
];
ReactDOM.render(
<div>
{
tracks.map((track) => {
return <div className="track">{track.title}</div>;
})
}
</div>,
document.getElementById('app')
);
module.hot.accept();

The JSX syntax takes getting used to. Basically we can use JavaScript in HTML. In our code snippet we map over a list of tracks and return a HTML node with track properties.

The console output gives the hint of a missing key property. React elements need that key property to uniquely identify themselves in a list of elements. Let’s fix this, save the file and see how hot reloading kicks in and refreshes our page!

import React from 'react';
import ReactDOM from 'react-dom';
const tracks = [
{
title: 'Some track'
},
{
title: 'Some other track'
}
];
ReactDOM.render(
<div>
{
tracks.map((track, key) => {
return <div className="track" key={key}>{track.title}</div>;
})
}
</div>,
document.getElementById('app')
);

Now it's time to write our first real component. We can extract the rendered list of tracks in an own component, because the src/index.js should be only seen as entry point to the React application.

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import Stream from './components/Stream';
const tracks = [
{
title: 'Some track'
},
{
title: 'Some other track'
}
];
ReactDOM.render(
<Stream tracks={tracks} />,
document.getElementById('app')
);
module.hot.accept();

We import a Stream component which gets a list of tracks as props. Moreover we use that component as first parameter for ReactDOM.render. Now let's implement the Stream component.

From src folder:

mkdir components
cd components
touch Stream.js

Our src folder is getting its first structure. We will organise our files by a technical separation - starting with a components folder, but later on adding more folders aside.

While it's good to have a technical separation of concerns in an early project, it may not scale for larger applications. You might want to consider to organise your app by features with a growing code base.

Let’s give our recent created file some content.

src/components/Stream.js

import React from 'react';
class Stream extends React.Component {
render() {
const { tracks = [] } = this.props;
return (
<div>
{
tracks.map((track, key) => {
return <div className="track" key={key}>{track.title}</div>;
})
}
</div>
);
}
}
export default Stream;

The Stream component is a React ES6 class component. The render shorthand function returns the element. Additionally we retrieve the props from this by using ES6 destructuring and providing a default empty list.

React ES6 class components provide a slim API. These lifecycle methods can be used to hook into the component lifecycle. For instance you can do things before a component gets rendered with componentWillMount() or when it updated with componentDidUpdate(). You can read about all component lifecycle methods.

class Stream extends React.Component {
render() {
...
}
componentWillMount() {
// do things
}
componentDidUpdate() {
// do things
}
}

ES6 class components can have internal component state. Imagine you could like a track. You would have to save the state whether a track is liked or not liked. I will demonstrate how you can achieve it.

import React from 'react';
class Stream extends React.Component {
constructor() {
super();
this.state = {};
}
render() {
const { tracks = [] } = this.props;
return (
<div>
{
tracks.map((track, key) => {
return (
<div className="track" key={key}>
{track.title}
<button onClick={() => this.setState({ [key]: !this.state[key] })} type="button">
{ this.state[key] ? 'Dislike' : 'Like' }
</button>
</div>
);
})
}
</div>
);
}
}
export default Stream;

You would need a contructor to setup the initial internal component state. Afterwards you can use setState() to modify the state and this.state to get the state. We modify the state in the onClick handler and get the state to show a button label.

Let's keep the state out of our component for the sake of simplicity.

src/components/Stream.js

import React from 'react';
class Stream extends React.Component {
render() {
const { tracks = [] } = this.props;
return (
<div>
{
tracks.map((track, key) => {
return <div className="track" key={key}>{track.title}</div>;
})
}
</div>
);
}
}
export default Stream;

Since we don't need internal component state nor lifecycle methods, we can refactor our ES6 class component to a stateless functional component.

src/components/Stream.js

import React from 'react';
function Stream({ tracks = [] }) {
return (
<div>
{
tracks.map((track, key) => {
return <div className="track" key={key}>{track.title}</div>;
})
}
</div>
);
}
export default Stream;

It's called stateless functional component, because it only gets an input and generates an output. There are no side effects happening (functional) and our component doesn’t know internal state at all (stateless). It's only a function which gets a state and returns a view: (State) => View.

You can use ES6 class components whenever you need component lifecycle methods or internal component state. If that's not the case, use functional stateless components.

Folder structure:

- dist
-- index.html
- node_modules
- src
-- components
--- Stream.js
-- index.js
- package.json
- webpack.config.js

It’s done. We have written our first React code!

A lot of things already happened during the last chapters. Let’s summarise these with some notes:

  • we use webpack + webpack-dev-server for bundling, building and serving our app
  • we use Babel
    • to write in ES6 syntax
    • to have .js rather than .jsx files
  • the src/index.js file is used by Webpack as entry point to bundle all of its used imports in one file named bundle.js
  • bundle.js is used in dist/index.html
  • dist/index.html provides us an identifier as entry point for our React root component
  • we set up our first React hook via the id attribute in src/index.js
  • we implemented our first component as stateless functional component src/components/Stream.js

You may want to experiment a bit more with React before you dive into Redux. Build some more ES6 class and functional stateless components. Additionally use lifecycle methods and internal component state to get used to it. Only then you will see the benefits of using Redux for state management.

Test Setup

I want to show you a simple setup to test your React components. I will do this by testing the Stream component, but later on I will not go any deeper into the topic of testing.

We will use mocha as test framework, chai as assertion library and jsdom to provide us with a pure JavaScript DOM implementation which runs in node.

From root folder:

npm install --save-dev mocha chai jsdom

Moreover we need a test setup file for some more configuration especially for our virtual DOM setup.

From root folder:

mkdir test
cd test
touch setup.js

test/setup.js

import React from 'react';
import { expect } from 'chai';
import jsdom from 'jsdom';
const doc = jsdom.jsdom('<!doctype html><html><body></body></html>');
const win = doc.defaultView;
global.document = doc;
global.window = win;
Object.keys(window).forEach((key) => {
if (!(key in global)) {
global[key] = window[key];
}
});
global.React = React;
global.expect = expect;

Essentially we are exposing globally a jsdom generated document and window object, which can be used by React during tests. Additionally we need to expose all properties from the window object that our running tests later on can use them. Last but not least we are giving global access to the objects React and expect. It helps us that we don’t have to import each of them in our tests.

In package.json we will have to add a new script to run our tests which respects Babel, uses mocha as test framework, uses our previously written test/setup.js file and traverses through all of our files within the src folder with a spec.js suffix.

package.json

...
"scripts": {
"start": "webpack-dev-server --progress --colors --hot --config ./webpack.config.js",
"test": "mocha --compilers js:babel-core/register --require ./test/setup.js 'src/**/*spec.js'"
},
...

Additionally there are some more neat libraries to help us with React component tests. Enzyme by Airbnb is a library to test React components. It relies on react-addons-test-utils and react-dom (the latter we already installed via npm).

Jest can be used alone or in combination with enzyme to test React components. It's the official library by Facebook.

From root folder:

npm install --save-dev react-addons-test-utils enzyme

Now we are set to write our first component test.

From components folder:

touch Stream.spec.js

src/components/Stream.spec.js

import Stream from './Stream';
import { shallow } from 'enzyme';
describe('Stream', () => {
const props = {
tracks: [{ title: 'x' }, { title: 'y' }],
};
it('shows two elements', () => {
const element = shallow(<Stream { ...props } />);
expect(element.find('.track')).to.have.length(2);
});
});

Here we are serving our Stream component with an array of two tracks. As we know both of these tracks should get rendered. The expect assertion checks whether we are rendering two DOM elements with the class track. When we run our tests, they should pass.

From root folder:

npm test

Moreover we can enhance our package.json scripts collection by a test:watch script.

package.json

...
"scripts": {
"start": "webpack-dev-server --progress --colors --hot --config ./webpack.config.js",
"test": "mocha --compilers js:babel-core/register --require ./test/setup.js ‘src/**/*spec.js’”,
"test:watch": "npm run test -- --watch"
},
...

By running the script we can see our tests executed every time we change something in our source code.

From root folder:

npm run test:watch

Folder structure:

- dist
-- index.html
- node_modules
- src
-- components
--- Stream.js
--- Stream.spec.js
-- index.js
- test
-- setup.js
- package.json
- webpack.config.js

We won't create anymore tests during this tutorial. As exercise feel free to add more tests during the next chapters!

Redux

Redux describes itself as predictable state container for JS apps. Most of the time you will see Redux coupled with React used in client side applications. But it's far more than that. Like JavaScript itself is spreading on server side applications or IoT applications, Redux can be used everywhere to have a predictable state container. You will see that Redux is not strictly coupled to React, because it has its own module, while you can install another module to connect it to the React world. There exist modules to connect Redux to other frameworks as well. Moreover the ecosystem around Redux itself is huge. Once you dive into it, you can learn tons of new stuff. Most of the time it is not only just another library: You have to look behind the facade to grasp which problem it will solve for you. Only then you should use it! When you don’t run into that problem, don’t use it. But be curious what is out there and how people get creative in that ecosystem!

At this point I want to show some respect to Dan Abramov, the inventor of Redux, who is not only providing us with a simple yet mature library to control our state, but also showing a huge contribution in the open source community on a daily basis. Watch his talk from React Europe 2016 where he speaks about the journey of Redux and what made Redux successful.

Redux Roundtrip

I call it the Redux Roundtrip, because it encourages you to use a unidirectional data flow. The Redux Roundtrip evolved from the flux architecture. Basically you trigger an action in a component, it could be a button, someone listens to that action, uses the payload of that action, and generates a new global state object which gets provided to all components. The components can update and the roundtrip is finished.

Let’s get started with Redux by implementing our first roundtrip!

From root folder:

npm install --save redux

Dispatching an Action

Let’s dispatch our first action and get some explanation afterwards.

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import configureStore from './stores/configureStore';
import * as actions from './actions';
import Stream from './components/Stream';
const tracks = [
{
title: 'Some track'
},
{
title: 'Some other track'
}
];
const store = configureStore();
store.dispatch(actions.setTracks(tracks));
ReactDOM.render(
<Stream />,
document.getElementById('app')
);
module.hot.accept();

As you can see we initialise a store object with some imported function we didn’t define yet. The store is a singleton Redux object and holds our global state object. Moreover it is possible to use a lightweight store API to dispatch an action, get the state of the store or subscribe to the store when updates occur.

In this case we are dispatching our first action with a payload of our hardcoded tracks. Since we want to wire our Stream component directly to the store later on, we don’t need to pass anymore the tracks as properties to our Stream component.

Where will we continue? Either we can define our configureStore function which generates the store object or we can have a look at our first dispatched action. We will continue with the latter by explaining actions and action creators, go over to reducers which will deal with the global state object and at the end set up our store which holds the global state object. After that our component can subscribe to the store to get updates or use the stores interface to dispatch new actions to modify the global state.

Constant Action Types

It is good to have a constants folder in general, but in early Redux projects you will often end up with some constants to identify your actions. These constants get shared by actions and reducers. In general it is a good approach to have all your action constants, which describe the change of your global state, at one place.

When your project grows, there exist other folder/file structure patterns to organise your Redux code.

From src folder:

mkdir constants
cd constants
touch actionTypes.js

src/constants/actionTypes.js

export const TRACKS_SET = 'TRACKS_SET';

Action Creators

Now we get to the action creators. They return an object with a type and a payload. The type is an action constant like the one we defined in our previous created action types. The payload can be anything which will be used to change the global state.

From src folder:

mkdir actions
cd actions
touch track.js

src/actions/track.js

import * as actionTypes from '../constants/actionTypes';
export function setTracks(tracks) {
return {
type: actionTypes.TRACKS_SET,
tracks
};
};

Our first action creator takes as input some tracks which we want to set to our global state. It returns an object with an action type and a payload.

To keep our folder structure tidy, we need to setup an entry point to our action creators via an index.js file.

From actions folder:

touch index.js

src/actions/index.js

import { setTracks } from './track';
export {
setTracks
};

In that file we can bundle all of our action creators to export them as public interface to the rest of the app. Whenever we need to access some action creator from somewhere else, we have a clearly defined interface for that, without reaching into every action creator file itself. We will do the same later on for our reducers.

Reducers

After we dispatched our first action and implemented our first action creator, someone must be aware of that action type to access the global state. These functions are called reducers, because they take an action with its type and payload and reduce it to a new state (previousState, action) => newState. Important: Rather than modifying the previousState, we return a new object newState - the state is immutable.

The state in Redux must be treated as immutable state. You will never modify the previous state and you will always return a new state object. You want to keep your data structure immutable to avoid any side effects in your application.

Let’s create our first reducer.

From src folder:

mkdir reducers
cd reducers
touch track.js

src/reducers/track.js

import * as actionTypes from '../constants/actionTypes';
const initialState = [];
export default function(state = initialState, action) {
switch (action.type) {
case actionTypes.TRACKS_SET:
return setTracks(state, action);
}
return state;
}
function setTracks(state, action) {
const { tracks } = action;
return [ ...state, ...tracks ];
}

As you can see we export an anonymous function, the reducer, as an interface to our existing app. The reducer gets a state and action like explained previously. Additionally you can define a default parameter as a function input. In this case we want to have an empty array as initial state.

The initial state is the place where you normally would put something like our hardcoded tracks from the beginning, rater than dispatching an action (because they are hardcoded). But later on, we want to replace these tracks with tracks we fetched from the SoundCloud API, and thus we have to set these tracks as state via an action.

The reducer itself has a switch case to differ between action types. Now we have only one action type, but this will grow by adding more action types in an evolving application.

After all we use the ES6 spread operator to put our previous state plus the action payload, in that case the tracks, in our returned new state. We are using the spread operator to keep our object immutable. I can recommend libraries like Immutable.js in the beginning to enforce the usage of immutable data structures, but for the sake of simplicity I will go on with pure ES6 syntax.

Again to keep our folder interfaces tidy, we create an entry point to our reducers.

From reducers folder:

touch index.js

src/reducers/index.js

import { combineReducers } from 'redux';
import track from './track';
export default combineReducers({
track
});

Saving us some refactoring, I already use a helper function combineReducers here. Normally you would start to export one plain reducer. That reducer would return the whole state. When you use combineReducers, you are able to have multiple reducers, where each reducer only returns a substate. Without combineReducers you would access your tracks in the global state with state.tracks. But with combineReducers you get these intermediate layer to get to the subset of states produced by multiple reducers. In that case state.track.tracks where track is our substate to handle all track states in the future.

Store with Global State

Now we dispatched our first action, implemented a pair of action type and action creator, and generated a new state via a reducer. What is missing is our store, which we already created from some not yet implemented function in our src/index.js.

Remember when we dispatched our first action via the store interface store.dispatch(actionCreator(payload))? The store is aware of the state and thus it is aware of our reducers with their state manipulations.

Let’s create the store file.

From src folder:

mkdir stores
cd stores
touch configureStore.js

src/stores/configureStore.js

import { createStore } from 'redux';
import rootReducer from '../reducers/index';
export default function configureStore(initialState) {
return createStore(rootReducer, initialState);
}

Redux provides us with a createStore function which takes the rootReducer and an initial state.

Let's add a store middleware to even the way to a mature Redux application.

src/stores/configureStore.js

import { createStore, applyMiddleware } from 'redux';
import rootReducer from '../reducers/index';
const createStoreWithMiddleware = applyMiddleware()(createStore);
export default function configureStore(initialState) {
return createStoreWithMiddleware(rootReducer, initialState);
}

The Redux store is aware of a middleware, which can be used to do something between dispatching an action and the moment it reaches the reducer. There is already a lot of middleware for Redux out there. Let's use the logger middleware for the beginning.

npm install --save redux-logger

The logger middleware shows us console output for each action: the previous state, the action itself and the next state. It helps us to keep track of our state changes in our application.

src/stores/configureStore.js

import { createStore, applyMiddleware } from 'redux';
import { createLogger } from 'redux-logger';
import rootReducer from '../reducers/index';
const logger = createLogger();
const createStoreWithMiddleware = applyMiddleware(logger)(createStore);
export default function configureStore(initialState) {
return createStoreWithMiddleware(rootReducer, initialState);
}

Let’s start our app again and see what happens.

From root folder:

npm start

In the browser we don’t see the tracks from our global store, because we don’t pass any global state to our Stream component yet. But we can see in the console output our first action which gets dispatched.

Let’s connect our Stream component to the Redux store to close the Redux Roundtrip.

Connect Redux and React

As I mentioned early there exist some libraries to wire Redux to other environments. Since we are using React, we want to connect Redux to our React components.

From root folder:

npm install --save react-redux

Do you remember when I told you about the lightweight Redux store API? We will never have the pleasure to enjoy the store.subscribe functionality to listen to store updates. With react-redux we are skipping that step and let this library take care of connecting our components to the store to listen to updates.

Essentially we need two steps to wire the Redux store to our components. Let’s begin with the first one.

Provider

The Provider from react-redux helps us to make the store and its functionalities available in all child components. The only thing we have to do is to initiate our store and wrap our child components within the Provider component. At the end the Provider component uses the store as property.

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import configureStore from './stores/configureStore';
import * as actions from './actions';
import Stream from './components/Stream';
const tracks = [
{
title: 'Some track'
},
{
title: 'Some other track'
}
];
const store = configureStore();
store.dispatch(actions.setTracks(tracks));
ReactDOM.render(
<Provider store={store}>
<Stream />
</Provider>,
document.getElementById('app')
);
module.hot.accept();

Now we made the Redux store available to all child components, in that case the Stream component.

Connect

The connect functionality from react-redux helps us to wire React components, which are embedded in the Provider helper component, to our Redux store. We can extend our Stream component as follows to get the required state from the Redux store.

Remember when we passed the hardcoded tracks directly to the Stream component? Now we set these tracks via the Redux Roundtrip in our global state and want to retrieve a part of this state in the Stream component.

src/components/Stream.js

import React from 'react';
import { connect } from 'react-redux';
function Stream({ tracks = [] }) {
return (
<div>
{
tracks.map((track, key) => {
return <div className="track" key={key}>{track.title}</div>;
})
}
</div>
);
}
function mapStateToProps(state) {
const tracks = state.track;
return {
tracks
}
}
export default connect(mapStateToProps)(Stream);

As you can see the component itself doesn’t change at all.

Basically we are using the returned function of connect to take our Stream component as argument to return a higher order component. The higher order component is able to access the Redux store while the Stream component itself is only presenting our data.

Additionally the connect function takes as first argument a mapStateToProps function which returns an object. The object is a substate of our global state. In mapStateToProps we are only exposing the substate of the global state which is required by the component.

Moreover it is worth to mention that we could still access properties given from parent components via <Stream something={thing} /> via the mapStateToProps function. The functions gives us as second argument these properties, which we could pass with out substate to the Stream component itself.

function mapStateToProps(state, props) {}

Now start your app and you should see this time the rendered list of tracks in your browser. We already saw these tracks in a previous step, but this time we retrieve them from our Redux store.

The test should break right now, but we will fix that in the next step.

Container and Presenter Component

Our Stream component has two responsibilities now. First it connects some state to our component and second it renders some DOM. We could split both into container and presenter component, where the container component is responsible to connect the component to the Redux world and the presenter component only renders some DOM.

Let’s refactor!

First we need to organise our folder. Since we will not only end up with one file for the Stream component, we need to set up a dedicated Stream folder with all its files.

From components folder:

mkdir Stream
cd Stream
touch index.js
touch presenter.js
touch spec.js

The Stream folder consists of an index.js file (container), presenter.js file (presenter) and spec.js file (test). Later on we could have style.css/less/scss, story.js etc. files in that folder as well.

Let’s refactor by each file. While every line of code is new in these files, I highlighted the important new parts coming with that refactoring. Most of the old code gets only separated in the new files.

src/components/Stream/index.js

import React from 'react';
import { connect } from 'react-redux';
import Stream from './presenter';
function mapStateToProps(state) {
const tracks = state.track;
return {
tracks
}
}
export default connect(mapStateToProps)(Stream);

src/components/Stream/presenter.js

import React from 'react';
function Stream({ tracks = [] }) {
return (
<div>
{
tracks.map((track, key) => {
return <div className="track" key={key}>{track.title}</div>;
})
}
</div>
);
}
export default Stream;

src/components/Stream/spec.js

import Stream from './presenter';
import { shallow } from 'enzyme';
describe('Stream', () => {
const props = {
tracks: [{ title: 'x' }, { title: 'y' }],
};
it('shows two elements', () => {
const element = shallow(<Stream { ...props } />);
expect(element.find('.track')).to.have.length(2);
});
});

Now you can delete the old files Stream.js and Stream.spec.js, because they got refactored into the new Stream folder.

When you start your app, you should still see the list of tracks rendered. Moreover the test should be fixed again.

In the last steps we finished the Redux Roundtrip and connected our components to the Redux environment. Now let’s dive into our real world application - the SoundCloud client.

SoundCloud App

There is nothing better than having an app with some real data showing up. Rather than having some hardcoded data to display, it is an awesome feeling to fetch some data from a well known service like SoundCloud.

In the chapter of this tutorial we will implement our SoundCloud client, which means that we login as SoundCloud user and show our latest track stream. Moreover we will be able to hit the play button for these tracks.

Registration

Before you can create a SoundCloud client, you need to have an account and register a new app. Visit Developers SoundCloud and click the “Register a new app” link. Give your app a name and “Register” it.

react redux

In the last registration step you give your app a “Redirect URI” to fulfil the registration later in the app via a login popup. Since we are developing locally, we will set this Redirect URI to “http://localhost:8080/callback”.

react redux

The port should be 8080 by default, but consider to change this according to your setup.

The previous step gives us two constants which we have to use in our app: Client ID and Redirect URI. We need both to setup our authentication process. Let’s transfer these constants into a file.

From constants folder:

touch auth.js

src/constants/auth.js

export const CLIENT_ID = '1fb0d04a94f035059b0424154fd1b18c'; // Use your client ID
export const REDIRECT_URI = `${window.location.protocol}//${window.location.host}/callback`;

Now we can authenticate with SoundCloud.

From root folder:

npm --save install soundcloud

src/index.js

import SC from 'soundcloud';
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import configureStore from './stores/configureStore';
import * as actions from './actions';
import Stream from './components/Stream';
import { CLIENT_ID, REDIRECT_URI } from './constants/auth';
SC.initialize({ client_id: CLIENT_ID, redirect_uri: REDIRECT_URI });
const tracks = [
{
title: 'Some track'
},
{
title: 'Some other track'
}
];
const store = configureStore();
store.dispatch(actions.setTracks(tracks));
ReactDOM.render(
<Provider store={store}>
<Stream />
</Provider>,
document.getElementById('app')
);
module.hot.accept();

React Router

The authentication process relies on a route called “/callback” in our app. Therefore we need to setup React Router to provide our app with some simple routing.

From root folder:

npm --save install react-router react-router-redux

You have to add the following line to your web pack configuration.

webpack.config.js

module.exports = {
entry: [
'webpack-dev-server/client?http://localhost:8080',
'webpack/hot/only-dev-server',
'./src/index.js'
],
module: {
loaders: [{
test: /\.jsx?$/,
exclude: /node_modules/,
loader: 'react-hot-loader!babel-loader'
}]
},
resolve: {
extensions: ['', '.js', '.jsx']
},
output: {
path: __dirname + '/dist',
publicPath: '/',
filename: 'bundle.js'
},
devServer: {
contentBase: './dist',
hot: true,
historyApiFallback: true
}
};

The historyApiFallback allows our app to do routing purely on the client side. Usually a route change would result into a server request to fetch new resources.

Let’s provide our app with two routes: one for our app, another one for the callback and authentication handling. Therefore we use some helper components provided by react-router. In general we have to specify path and component pairs. Therefore we define to see the Stream component on the root path “/” and the Callback component on “/callback” (that’s where the authentication happens). Additionally we can specify a wrapper component like App. We will see during its implementation, why it is good to have a wrapper component like App. Moreover we use react-router-redux to synchronise the browser history with the store. This would help us to react to route changes.

src/index.js

import SC from 'soundcloud';
import React from 'react';
import ReactDOM from 'react-dom';
import { Router, Route, IndexRoute, browserHistory } from 'react-router';
import { syncHistoryWithStore } from 'react-router-redux';
import { Provider } from 'react-redux';
import configureStore from './stores/configureStore';
import * as actions from './actions';
import App from './components/App';
import Callback from './components/Callback';
import Stream from './components/Stream';
import { CLIENT_ID, REDIRECT_URI } from './constants/auth';
SC.initialize({ client_id: CLIENT_ID, redirect_uri: REDIRECT_URI });
const tracks = [
{
title: 'Some track'
},
{
title: 'Some other track'
}
];
const store = configureStore();
store.dispatch(actions.setTracks(tracks));
const history = syncHistoryWithStore(browserHistory, store);
ReactDOM.render(
<Provider store={store}>
<Router history={history}>
<Route path="/" component={App}>
<IndexRoute component={Stream} />
<Route path="/" component={Stream} />
<Route path="/callback" component={Callback} />
</Route>
</Router>
</Provider>,
document.getElementById('app')
);
module.hot.accept();

At the end there are two new components: App as component wrapper and Callback for the authentication. Let’s create the first one.

From components folder:

mkdir App
cd App
touch index.js

src/components/App/index.js

import React from 'react';
function App({ children }) {
return <div>{children}</div>;
}
export default App;

App does not much here but passing all children. We will not use this component in this tutorial anymore, but in future implementations you could use this component to have static Header, Footer, Playlist or Player components while the children are changing.

Let’s create our Callback component.

From components folder:

mkdir Callback
cd Callback
touch index.js

src/components/Calback/index.js

import React from 'react';
class Callback extends React.Component {
componentDidMount() {
window.setTimeout(opener.SC.connectCallback, 1);
}
render() {
return <div><p>This page should close soon.</p></div>;
}
}
export default Callback;

That’s the default implementation to create the callback for the SoundCloud API. We do not need to touch this file anymore in the future.

The last step for the Router setup is to provide our store with the route state when we navigate from page to page.

src/reducers/index.js

import { combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux';
import track from './track';
export default combineReducers({
track,
routing: routerReducer
});

src/stores/configureStore.js

import { createStore, applyMiddleware } from 'redux';
import { createLogger } from 'redux-logger';
import { browserHistory } from 'react-router';
import { routerMiddleware } from 'react-router-redux';
import rootReducer from '../reducers/index';
const logger = createLogger();
const router = routerMiddleware(browserHistory);
const createStoreWithMiddleware = applyMiddleware(router, logger)(createStore);
export default function configureStore(initialState) {
return createStoreWithMiddleware(rootReducer, initialState);
}

Moreover we sync our store with the browser history, so that we can listen later on to events based on our current route. We will not use that in this tutorial, but it can help you to fetch data on route changes for instance. Additionally properties like browser path or query params in the URL can be accessed in the store now.

Authentication

Let’s authenticate with SoundCloud! We need to setup a new action to trigger that an event to authenticate. Let’s expose the auth function already and add the required action file afterwards.

src/actions/index.js

import { auth } from './auth';
import { setTracks } from './track';
export {
auth,
setTracks
};

From actions folder:

touch auth.js

src/actions/auth.js

import SC from 'soundcloud';
export function auth() {
SC.connect().then((session) => {
fetchMe(session);
});
};
function fetchMe(session) {
fetch(`//api.soundcloud.com/me?oauth_token=${session.oauth_token}`)
.then((response) => response.json())
.then((data) => {
console.log(data);
});
}

We are able to connect to the SoundCloud API, login with our credentials and see our account details in the console output.

Nobody is triggering that action though, so let’s do that for the sake of simplicity in our Stream component.

src/components/Stream/index.js

import React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as actions from '../../actions';
import Stream from './presenter';
function mapStateToProps(state) {
const tracks = state.track;
return {
tracks
}
}
function mapDispatchToProps(dispatch) {
return {
onAuth: bindActionCreators(actions.auth, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Stream);

In our container component we did only map some state to our presenter component. Now it comes to a second function we can pass to the connect function: mapDispatchToProps. This function helps us to pass actions to our presenter component. Within the mapDispatchToProps we return an object with functions, in this case one function named onAuth, and use our previously created action auth within that. Moreover we need to bind our action creator with the dispatch function.

Now let’s use this new available action in our presenter component.

src/components/Stream/presenter.js

import React from 'react';
function Stream({ tracks = [], onAuth }) {
return (
<div>
<div>
<button onClick={onAuth} type="button">Login</button>
</div>
<br/>
<div>
{
tracks.map((track, key) => {
return <div className="track" key={key}>{track.title}</div>;
})
}
</div>
</div>
);
}
export default Stream;

We simply put in a button and pass the onAuth function as onClick handler. After we start our app again, we should see the current user in the console output after we clicked the Login button. Additionally we will still see some error message, because our action goes nowhere, since we didn’t supply a according reducer for it.

We might need to install a polyfill for fetch, because some browser do not support the fetch API yet.

From root folder:

npm --save install whatwg-fetch
npm --save-dev install imports-loader exports-loader

webpack.config.js

var webpack = require('webpack');
module.exports = {
entry: [
'webpack-dev-server/client?http://localhost:8080',
'webpack/hot/only-dev-server',
'./src/index.js'
],
module: {
loaders: [{
test: /\.jsx?$/,
exclude: /node_modules/,
loader: 'react-hot-loader!babel-loader'
}]
},
resolve: {
extensions: ['*', '.js', '.jsx']
},
output: {
path: __dirname + '/dist',
publicPath: '/',
filename: 'bundle.js'
},
devServer: {
contentBase: './dist',
hot: true,
historyApiFallback: true
},
plugins: [
new webpack.ProvidePlugin({
'fetch': 'imports-loader?this=>global!exports-loader?global.fetch!whatwg-fetch'
})
]
};

Redux Thunk

We can see our current user object in the console output, but we don’t store it yet! Moreover we are using our first asynchronous action, because we have to wait for the SoundCloud server to respond our request. The Redux environment provides several middleware to deal with asynchronous actions (see list below). One of them is redux-thunk. The thunk middleware returns you a function instead of an action. Since we deal with an asynchronous call, we can delay the dispatch function with the middleware. Moreover the inner function gives us access to the store functions dispatch and getState.

Building React Applications with Idiomatic Redux by egghead.io and Dan Abramov shows you how to implement your own thunk middleware.

Some side-effect middleware in Redux:

From root folder:

npm --save install redux-thunk

Let’s add thunk as middleware to our store.

src/stores/configurationStore.js

import { createStore, applyMiddleware } from 'redux';
import { createLogger } from 'redux-logger';
import thunk from 'redux-thunk';
import { browserHistory } from 'react-router';
import { routerMiddleware } from 'react-router-redux'
import rootReducer from '../reducers/index';
const logger = createLogger();
const router = routerMiddleware(browserHistory);
const createStoreWithMiddleware = applyMiddleware(thunk, router, logger)(createStore);
export default function configureStore(initialState) {
return createStoreWithMiddleware(rootReducer, initialState);
}

Set Me

Now we have everything in place to save our user object to the store. Therefore we need to create a new set of action type, action creator and reducer.

src/constants/actionTypes.js

export const ME_SET = 'ME_SET';
export const TRACKS_SET = 'TRACKS_SET';

src/actions/auth.js

import SC from 'soundcloud';
import * as actionTypes from '../constants/actionTypes';
function setMe(user) {
return {
type: actionTypes.ME_SET,
user
};
}
export function auth() {
return function (dispatch) {
SC.connect().then((session) => {
dispatch(fetchMe(session));
});
};
};
function fetchMe(session) {
return function (dispatch) {
fetch(`//api.soundcloud.com/me?oauth_token=${session.oauth_token}`)
.then((response) => response.json())
.then((data) => {
dispatch(setMe(data));
});
};
}

Instead of doing the console output when we retrieved the user object, we simply call our action creator. Moreover we can see that the thunk middleware requires us to return a function instead of an object. The function gives us access to the dispatch functionality of the store.

Let's add the new reducer.

src/reducers/index.js

import { combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux';
import auth from './auth';
import track from './track';
export default combineReducers({
auth,
track,
routing: routerReducer
});

From reducers folder:

touch auth.js

src/reducers/auth.js

import * as actionTypes from '../constants/actionTypes';
const initialState = {};
export default function(state = initialState, action) {
switch (action.type) {
case actionTypes.ME_SET:
return setMe(state, action);
}
return state;
}
function setMe(state, action) {
const { user } = action;
return { ...state, user };
}

The reducer respects the new action type and returns a newState with our user in place.

Now we want to see visually in our DOM whether the login was successful. Therefor we can exchange the Login button once the login itself was successful.

src/components/Stream/index.js

import React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as actions from '../../actions';
import Stream from './presenter';
function mapStateToProps(state) {
const { user } = state.auth;
const tracks = state.track;
return {
user,
tracks
}
}
function mapDispatchToProps(dispatch) {
return {
onAuth: bindActionCreators(actions.auth, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Stream);

In our container component we map our new state, the current user, to the presenter component.

src/components/Stream/presenter.js

import React from 'react';
function Stream({ user, tracks = [], onAuth }) {
return (
<div>
<div>
{
user ?
<div>{user.username}</div> :
<button onClick={onAuth} type="button">Login</button>
}
</div>
<br/>
<div>
{
tracks.map((track, key) => {
return <div className="track" key={key}>{track.title}</div>;
})
}
</div>
</div>
);
}
export default Stream;

The presenter component decides whether it has to show the username or the Login button. When we start our app again and login, we should the displayed username instead of a button.

From root folder:

npm start

Fetch Tracks

Now we are authenticated with the SoundCloud server. Let’s get real and fetch some real tracks and replace the hardcoded tracks.

src/index.js

import SC from 'soundcloud';
import React from 'react';
import ReactDOM from 'react-dom';
import { Router, Route, IndexRoute, browserHistory } from 'react-router';
import { syncHistoryWithStore } from 'react-router-redux';
import { Provider } from 'react-redux';
import configureStore from './stores/configureStore';
import App from './components/App';
import Callback from './components/Callback';
import Stream from './components/Stream';
import { CLIENT_ID, REDIRECT_URI } from './constants/auth';
SC.initialize({ client_id: CLIENT_ID, redirect_uri: REDIRECT_URI });
const store = configureStore();
const history = syncHistoryWithStore(browserHistory, store);
ReactDOM.render(
<Provider store={store}>
<Router history={history}>
<Route path="/" component={App}>
<IndexRoute component={Stream} />
<Route path="/" component={Stream} />
<Route path="/callback" component={Callback} />
</Route>
</Router>
</Provider>,
document.getElementById('app')
);
module.hot.accept();

We only removed the hardcoded tracks in here. Moreover we don’t dispatch anymore an action to set some initial state.

src/actions/auth.js

import SC from 'soundcloud';
import * as actionTypes from '../constants/actionTypes';
import { setTracks } from '../actions/track';
function setMe(user) {
return {
type: actionTypes.ME_SET,
user
};
}
export function auth() {
return function (dispatch) {
SC.connect().then((session) => {
dispatch(fetchMe(session));
dispatch(fetchStream(session));
});
};
};
function fetchMe(session) {
return function (dispatch) {
fetch(`//api.soundcloud.com/me?oauth_token=${session.oauth_token}`)
.then((response) => response.json())
.then((data) => {
dispatch(setMe(data));
});
};
}
function fetchStream(session) {
return function (dispatch) {
fetch(`//api.soundcloud.com/me/activities?limit=20&offset=0&oauth_token=${session.oauth_token}`)
.then((response) => response.json())
.then((data) => {
dispatch(setTracks(data.collection));
});
};
}

After the authentication we simply dispatch a new asynchronous action to fetch track data from the SoundCloud API. Since we already had an action creator to set tracks in our state, wen can reuse this.

The returned data hasn’t only the list of tracks, it has some more meta data which could be used to fetch more paginated data afterwards. You would have to save the next_href property of data to do that.

The data structure of the SoundCloud tracks looks a bit different than our hardcoded tracks before. We need to change that in our Stream presenter component.

src/components/Stream/presenter.js

import React from 'react';
function Stream({ user, tracks = [], onAuth }) {
return (
<div>
<div>
{
user ?
<div>{user.username}</div> :
<button onClick={onAuth} type="button">Login</button>
}
</div>
<br/>
<div>
{
tracks.map((track, key) => {
return <div className="track" key={key}>{track.origin.title}</div>;
})
}
</div>
</div>
);
}
export default Stream;

Moreover we need to adjust our test that it respects the new track data structure.

src/components/Stream/spec.js

import Stream from './presenter';
import { shallow } from 'enzyme';
describe('Stream', () => {
const props = {
tracks: [{ origin: { title: 'x' } }, { origin: { title: 'y' } }],
};
it('shows two elements', () => {
const element = shallow(<Stream { ...props } />);
expect(element.find('.track')).to.have.length(2);
});
});

When you start your app now, you should see some tracks from your personal stream listed after the login.

Even if you created a new SoundCloud account, I hope you have a stream displayed though. If you get some empty stream data, you have to use SoundCloud directly to generate some e.g. via following some people.

From root folder:

npm start

SoundCloud Player

How would it be to have your own audio player within the browser? Therefor the last step in this tutorial is to make the tracks playable!

Another Redux Roundtrip

You should be already familiar with the procedure of creating action, action creator and reducer. Moreover you have to trigger that from within a component. Let’s start by providing our Stream component some yet not existing onPlay functionality. Moreover we will display a Play button next to each track which triggers that functionality.

src/components/Stream/presenter.js

import React from 'react';
function Stream({ user, tracks = [], onAuth, onPlay }) {
return (
<div>
<div>
{
user ?
<div>{user.username}</div> :
<button onClick={onAuth} type="button">Login</button>
}
</div>
<br/>
<div>
{
tracks.map((track, key) => {
return (
<div className="track" key={key}>
{track.origin.title}
<button type="button" onClick={() => onPlay(track)}>Play</button>
</div>
);
})
}
</div>
</div>
);
}
export default Stream;

In our container Stream component we can map that action to the presenter component.

src/components/Stream/index.js

import React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as actions from '../../actions';
import Stream from './presenter';
function mapStateToProps(state) {
const { user } = state.auth;
const tracks = state.track;
return {
user,
tracks
}
};
function mapDispatchToProps(dispatch) {
return {
onAuth: bindActionCreators(actions.auth, dispatch),
onPlay: bindActionCreators(actions.playTrack, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Stream);

Now we will have to implement the non existent playTrack action creator.

src/actions/index.js

import { auth } from './auth';
import { setTracks, playTrack } from './track';
export {
auth,
setTracks,
playTrack
};

src/actions/track.js

import * as actionTypes from '../constants/actionTypes';
export function setTracks(tracks) {
return {
type: actionTypes.TRACKS_SET,
tracks
};
};
export function playTrack(track) {
return {
type: actionTypes.TRACK_PLAY,
track
};
}

Don’t forget to export a new action type as constant.

src/constants/actionTypes.js

export const ME_SET = 'ME_SET';
export const TRACKS_SET = 'TRACKS_SET';
export const TRACK_PLAY = 'TRACK_PLAY';

In our reducer we make place for another initial state. In the beginning there will be no active track set, but when we trigger to play a track, the track should be set as activeTrack.

src/reducers/track.js

import * as actionTypes from '../constants/actionTypes';
const initialState = {
tracks: [],
activeTrack: null
};
export default function(state = initialState, action) {
switch (action.type) {
case actionTypes.TRACKS_SET:
return setTracks(state, action);
case actionTypes.TRACK_PLAY:
return setPlay(state, action);
}
return state;
}
function setTracks(state, action) {
const { tracks } = action;
return { ...state, tracks };
}
function setPlay(state, action) {
const { track } = action;
return { ...state, activeTrack: track };
}

Additionally we want to show the currently played track, therefore we need to map the activeTrack in our Stream container component.

src/components/Stream/index.js

import React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as actions from '../../actions';
import Stream from './presenter';
function mapStateToProps(state) {
const { user } = state.auth;
const { tracks, activeTrack } = state.track;
return {
user,
tracks,
activeTrack
}
};
function mapDispatchToProps(dispatch) {
return {
onAuth: bindActionCreators(actions.auth, dispatch),
onPlay: bindActionCreators(actions.playTrack, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Stream);

By starting our app, we should be able to login, to see our tracks and to play a track. The redux-logger should show some console output that we have set an activeTrack. But there is no music yet! Let’s implement that!

Listen to the music!

In our last step we already handed the activeTrack to our presenter Stream component. Let’s see what we can do about that.

src/components/Stream/presenter.js

import React from 'react';
import { CLIENT_ID } from '../../constants/auth';
function Stream({ user, tracks = [], activeTrack, onAuth, onPlay }) {
return (
<div>
<div>
{
user ?
<div>{user.username}</div> :
<button onClick={onAuth} type="button">Login</button>
}
</div>
<br/>
<div>
{
tracks.map((track, key) => {
return (
<div className="track" key={key}>
{track.origin.title}
<button type="button" onClick={() => onPlay(track)}>Play</button>
</div>
);
})
}
</div>
{
activeTrack ?
<audio id="audio" ref="audio" src={`${activeTrack.origin.stream_url}?client_id=${CLIENT_ID}`}></audio> :
null
}
</div>
);
}
export default Stream;

We need the CLIENT_ID to authenticate the audio player with the SoundCloud API in order to stream a track via its stream_url. In React 15 you can return null, when there is no activeTrack. In older versions you had to return <noscript />.

When we start our app and try to play a track, the console output says that we cannot define refs on stateless functional components. But we need that reference on the audio element to be able to use its audio API. Let’s transform the Stream presenter component to a stateful component. We will see how it gives us control over the audio element.

After all you should avoid to have stateful components and try to stick to functional stateless components. In this case we have no other choice.

src/components/Stream/presenter.js

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { CLIENT_ID } from '../../constants/auth';
class Stream extends Component {
componentDidUpdate() {
const audioElement = ReactDOM.findDOMNode(this.refs.audio);
if (!audioElement) { return; }
const { activeTrack } = this.props;
if (activeTrack) {
audioElement.play();
} else {
audioElement.pause();
}
}
render () {
const { user, tracks = [], activeTrack, onAuth, onPlay } = this.props;
return (
<div>
<div>
{
user ?
<div>{user.username}</div> :
<button onClick={onAuth} type="button">Login</button>
}
</div>
<br/>
<div>
{
tracks.map((track, key) => {
return (
<div className="track" key={key}>
{track.origin.title}
<button type="button" onClick={() => onPlay(track)}>Play</button>
</div>
);
})
}
</div>
{
activeTrack ?
<audio id="audio" ref="audio" src={`${activeTrack.origin.stream_url}?client_id=${CLIENT_ID}`}></audio> :
null
}
</div>
);
}
}
export default Stream;

Let’s start our app again. We login, we see our tracks as a list, we are able to hit the play button, we listen to music! I hope it works for you!

What's next?

Add one of the following tutorials on top of your current SoundCloud project:

Troubleshoot

In case you want to know which versions npm installed during that tutorial, here a list of all npm packages in my package.json.

package.json

"devDependencies": {
"babel-core": "^6.23.1",
"babel-loader": "^6.3.2",
"babel-preset-es2015": "^6.22.0",
"babel-preset-react": "^6.23.0",
"babel-preset-stage-2": "^6.22.0",
"chai": "^3.5.0",
"enzyme": "^2.7.1",
"exports-loader": "^0.6.3",
"imports-loader": "^0.7.0",
"jsdom": "^9.11.0",
"mocha": "^3.2.0",
"react-addons-test-utils": "^15.4.2",
"react-hot-loader": "^1.3.1",
"webpack": "^2.2.1",
"webpack-dev-server": "^2.4.1"
},
"dependencies": {
"react": "^15.4.2",
"react-dom": "^15.4.2",
"react-redux": "^5.0.2",
"react-router": "^3.0.2",
"react-router-redux": "^4.0.8",
"redux": "^3.6.0",
"redux-logger": "^3.0.0",
"redux-thunk": "^2.2.0",
"soundcloud": "^3.1.2",
"whatwg-fetch": "^2.0.2"
}

Final Thoughts

Hopefully you enjoyed this tutorial and learned a lot like I did. I didn’t plan to write so much in the first place, but I hope at the end it reaches enough people to encourage them to learn something new or simply to setup their own project.

I am open for feedback or bug reports on this tutorial. Please comment directly or reach out on Twitter.

Moreover have a look again at favesound-redux. Feel free to try it, to contribute, to raise issues when you find bugs or to use it as blueprint for your own application.

In conclusion keep an eye on that tutorial. I will add more smaller content in the future. Have a look at the next chapter for more information.

Contribute

I already mentioned it, but feel free to contribute to favesound-redux. Get in contact with me, there is plenty of stuff to do and it gives you a start into the open source community.

Moreover I want to extend this tutorial with smaller tutorials on top. Like I explained in Tutorial Extensions you can contribute in this repository and add your own folder in there which builds on top of the init folder. In your own folder you can address a new topic. There is a lot of potential!

Keep reading about 

Components become more important these days. In the future you will get to hear more and more about Web Components, which get available in Angular 2.0 as well, to create different reusable components…

I did a lot of Angular 1.x back in the days until I started to use React. I can say that I used both solutions extensively. But there were and are several reasons why I moved to React. These reasons…

The Road to React

Learn React by building real world applications. No setup configuration. No tooling. Plain React in 200+ pages of learning material. Learn React like 50.000+ readers.

Get it on Amazon.