神刀安全网

Huge Redux and React Real World Tutorial

Huge Redux and React Real World Tutorial

In the beginning of 2016 it was time for me to deep dive into the React 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 FaveSound a simple SoundCloud client . 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.

Table of Content

  1. A project from scratch
  2. First React Component
    1. Dispatching an Action
    2. Constant Action Types
    3. Store with Global State
  3. Connect Redux and React

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 or seed project from someone else. 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.

Requirements

Tools and versions I used during the implementation of FaveSound :

  • terminal (e.g. iTerm)
  • text editor (e.g. Sublime Text)
  • node with npm
node --version *v5.0.0 npm --version *v3.3.6

Let’s get started

The goal for this chapter is to setup the project. We are creating a new folder and initialise it as a npm project.

In your terminal type:

mkdir sc-react-redux cd sc-react-redux npm init -y

The last command should have generated a package.json file, where things like installed packages but also build scripts will end up during that tutorial.

Now we are creating a distribution folder where we later on serve our single page app (SPA). The whole SPA consists later of only two files: a .html and a .js file. While the .js file will be generated from all of our source files (via Webpack) later, we can already create the .html file as an entry point for our app.

Note: The distribution folder will be everything you need to publish your web app to a hosting server like I did with FaveSound .

From root folder:

mkdir dist cd dist touch index.html

dist/index.html

<!DOCTYPE html> <html>   <head>       <title>SoundCloud React Redux App</title>   </head>   <body>     <div id="app"></div>     <script src="bundle.js"></script>   </body> </html>

Two important notes:

  • the bundle.js file will be a generated file by Webpack (1)
  • the id=“app” attribute will help our root React component to find its entry point (2)

Possible next steps in this tutorial:

  • (1) Setup Webpack to bundle our source files in one file: bundle.js
  • (2) Build our first React root component which uses our entry point: id=“app”

Let’s continue with the first step followed by the latter one.

Webpack Setup

We will use Webpack as module bundler and build tool. Moreover we will use webpack-dev-server to serve our bundled app in a localhost environment.

From root folder:

npm install --save-dev webpack webpack-dev-server

In the following I will sometimes reference the folder structure for a better overview of our app structure.

Folder structure:

- dist -- index.html - node_modules - package.json

In the package.json file we can add a start script additionally to the default given scripts to run our webpack-dev-server.

package.json

... "scripts": {     "start": "webpack-dev-server --progress --colors --hot --config ./webpack.config.js",     ... }, ...

We define that we want to use the webpack-dev-server with some basic configuration and a configuration file called wepback.config.js. Let’s create that new required file.

From root folder:

touch webpack.config.js

webpack.config.js

module.exports = {   entry: [     './src/index.js'   ],   output: {     path: __dirname + '/dist',     publicPath: '/',     filename: 'bundle.js'   },   devServer: {     contentBase: './dist'   } };

Roughly the configuration file says that (1) we want to use the src/index.js file as entry point to bundle all of its imported files. (2) The bundled files will result in a bundle.js file which (3) will be generated in our already set up /dist folder. The /dist folder will be used to serve our app.

What is missing in our project is the src/index.js file.

From root folder:

mkdir src cd src touch index.js

src/index.js

console.log('My SoundCloud React Redux App');

Folder structure:

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

Now we are able to start our webpack-dev-server, open the app in a browser (default localhost:8080) and see our console output in the developer console.

From root folder:

npm start

We are serving our app via Webpack! We bundle our entry point file src/index.js as bundle.js, use it in dist/index.html and can see the generated console output in the developer console.

Hot reloading

A huge development boost will give us react-hot-loader , which will shorten our feedback loop during development. Basically whenever we change something in our source code, the change will apply in our app running in the browser without reloading the entire page .

From root folder:

npm install --save-dev react-hot-loader

webpack.config.js

module.exports = {   entry: [     'webpack-dev-server/client?http://localhost:8080',     'webpack/hot/only-dev-server',     './src/index.js'   ],   output: {     path: __dirname + '/dist',     publicPath: '/',     filename: 'bundle.js'   },   devServer: {     contentBase: './dist',     hot: true   } };

From root folder:

npm start

You should still see the console output in your developer console, but this time with some more output about hot reloading. You are almost done to write your first React component, but one building block is missing: We need to set up Babel.

Babel

Babel enables us writing our code in ES6 (ES2015) . Later on the code will get transpiled to ES5 that every browser, without having all ES6 features implemented, can interpret it.

Moreover we want to use some more experimental features in ES6 (e.g. object spread ) which can get activated via stages .

As last step, since we are using React, we need one more configuration to transform the natural React .jsx files to .js files for convenience.

From root folder:

npm install --save-dev babel-core babel-loader babel-preset-es2015 babel-preset-react babel-preset-stage-2

We need to adjust our package.json and webpack.config.js to respect the Babel changes. These changes include all features I listed above.

package.json

...   "babel": {     "presets": [       "es2015",       "react",       "stage-2"     ]   }, ...

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!babel'     }]   },   resolve: {     extensions: ['', '.js', '.jsx']   },   output: {     path: __dirname + '/dist',     publicPath: '/',     filename: 'bundle.js'   },   devServer: {     contentBase: './dist',     hot: true   } };

The npm start script should be broken right now, because our application doesn’t know about React yet. But we are ready to build our first React component, so let’s fix this!

First React Component

After having installed react and react-dom our build should be fine again.

From root folder:

npm install --save react react-dom

In our src/index.js we can implement our first hook into the React world rather than having a boring console output.

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') );

We are hooking via an ReactDOM.render function into our index.html file by getting the element with an id called “app”. Our first component Stream will be rendered at that place. Moreover we provide our Stream component with an array of hardcoded track objects (which makes sense, since we are going to write a SoundCloud client application). The Stream component itself will render the list of tracks. But we don’t have that Stream component yet, so let’s implement it!

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.

Note: While this is good for a tutorial and early project, you may want to consider to organise your app by features with a growing code base .

Let’s modify our recent created file.

src/components/Stream.js

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

Our Stream component is a stateless functional component . It gets tracks as property which default to an empty array (possible due ES6). The component maps over the tracks array and returns divs with the track.title as content. It is called stateless functional component, because it only gets an input and generates an output. There are no side effects happening (functional) and our Stream component doesn’t know the application state at all (stateless). It is only a function which gets an input and generates an output – more specific we have a function which gets a state and returns a view: (State) => View.

Folder structure:

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

When you start your app again, you should be able to see two track titles rendered in the browser. Moreover the console output gives the hint of a missing key property. React components need that key property to uniquely identify themselves in a list of components . Let’s fix this, save the file and see how hot reloading kicks in and refreshes our page!

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 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 child component as stateless functional component src/components/Stream.js

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 the status quo library at that point to test React components. It relies on react-addons-test-utils and react-dom (the latter we already installed via npm). Let’s install the other ones.

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 but adjusting the one we already have. Feel free to add more tests during the next chapters!

Redux

Redux describes itself as predictable state container for JavaScript apps. Most of the time you will see Redux coupled with React used in client side applications. But it is 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 Loop

I call it the Redux Loop, because it encourages you to use a unidirectional data flow. The Redux Loop 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 loop is finished.

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

From root folder:

npm install --save redux redux-logger

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') );

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.

Note: When your project grows, there exist other folder structure patterns to organise your action constants.

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 pattern of action creators. Action creators 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 previousState and you will always return a newState 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, since ES6, a default parameter as a function input. In this case we want to have an empty array as initial state.

Note: 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 previousState plus the action payload, in that case the tracks, in our returned newState. 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 like

state.tracks

But with combineReducers you get these intermediate layer to get to the subset of states produced by multipel 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, 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); }

Redux provides us with a createStore function. After all we would only need this function with our combined reducers to create a store store = createStore(rootReducer, initialState = []).

In our file there happens a bit more than that to even the way to a mature Redux application. 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, but we are only using the logger middleware for the beginning. The logger middleware shows us console output for each action: the previousState, the action itself and the nextState. It helps us to keep track of our state changes in our application.

In the end we are using the applyMiddleware helper function from Redux to wire our store to the middleware we specified.

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 loop.

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') );

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 loop 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 loop 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.

Huge Redux and React Real World Tutorial

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”.

Huge Redux and React Real World Tutorial

Note: 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`;

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!babel'     }]   },   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 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';  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') );

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.

Authentification

Let’s 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 { 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';  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') );

Since we want to authenticate with our SoundCloud account, we need to setup a new action to trigger that event. 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 { CLIENT_ID, REDIRECT_URI } from '../constants/auth';  export function auth() {     SC.initialize({ client_id: CLIENT_ID, redirect_uri: REDIRECT_URI });      SC.connect().then((session) => {       fetch(`//api.soundcloud.com/me?oauth_token=${session.oauth_token}`)         .then((response) => response.json())         .then((me) => {           console.log(me);         });     }); };

At first we import both constants CLIENT_ID and REDIRECT_URI. We can use these to initialise the SoundCloud client. After that 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.

Note: 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!babel'     }]   },   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?this=>global!exports?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. 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.

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 { CLIENT_ID, REDIRECT_URI } from '../constants/auth'; import * as actionTypes from '../constants/actionTypes';  function setMe(user) {   return {     type: actionTypes.ME_SET,     user   }; }  export function auth() {   return function (dispatch) {     SC.initialize({ client_id: CLIENT_ID, redirect_uri: REDIRECT_URI });      SC.connect().then((session) => {       fetch(`//api.soundcloud.com/me?oauth_token=${session.oauth_token}`)         .then((response) => response.json())         .then((me) => {           dispatch(setMe(me));         });     });   }; };

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';  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') );

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 { CLIENT_ID, REDIRECT_URI } from '../constants/auth'; 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.initialize({ client_id: CLIENT_ID, redirect_uri: REDIRECT_URI });      SC.connect().then((session) => {       fetch(`//api.soundcloud.com/me?oauth_token=${session.oauth_token}`)         .then((response) => response.json())         .then((me) => {           dispatch(setMe(me));           dispatch(fetchStream(me, session));         });     });   }; };  function fetchStream(me, 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.

Note: 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.

Note: 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!

New Redux Loop

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.

Note: 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!

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.9.1",     "babel-loader": "^6.2.4",     "babel-preset-es2015": "^6.9.0",     "babel-preset-react": "^6.5.0",     "babel-preset-stage-2": "^6.5.0",     "chai": "^3.5.0",     "enzyme": "^2.3.0",     "exports-loader": "^0.6.3",     "imports-loader": "^0.6.5",     "jsdom": "^9.2.1",     "mocha": "^2.5.3",     "react-addons-test-utils": "^15.1.0",     "react-hot-loader": "^1.3.0",     "webpack": "^1.13.1",     "webpack-dev-server": "^1.14.1"   },   "dependencies": {     "react": "^15.1.0",     "react-dom": "^15.1.0",     "react-redux": "^4.4.5",     "react-router": "^2.4.1",     "react-router-redux": "^4.0.5",     "redux": "^3.5.2",     "redux-logger": "^2.6.1",     "redux-thunk": "^2.1.0",     "soundcloud": "^3.1.2",     "whatwg-fetch": "^1.0.0"   }

Final Words

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 have to thank people like Christopher Chedeau and Dan Abramov who encourage people like me in their talks to contribute more in open source. Moreover I’d like to thank Tero Parviainen for his incredible Full-Stack Redux Tutorial which gave me an entry point to React and Redux.

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 use it , to contribute, to raise issues when you find bugs or to use it as blueprint for your own application.

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Huge Redux and React Real World Tutorial

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址