Creating a personal dashboard with React and Webpack

Published by at 15th August 2016 10:18 pm

The Raspberry Pi is a great device for running simple web apps at home on a permanent basis, and you can pick up a small touchscreen for it quite cheaply. This makes it easy to build and host a small personal dashboard that pulls important data from various APIs or RSS feeds and displays it. You'll often see dashboards like this on Raspberry Pi forums and subreddits. As I'm currently between jobs, and have some time to spare before my new job starts, I decided to start creating my own version of it. It was obvious that React.js is a good fit for this as it allows you to break up your user interface into multiple independent components and keep the functionality close to the UI. It also makes it easy to reuse widgets by passing different parameters through each time.

In this tutorial I'll show you how to start building a simple personal dashboard using React and Webpack. You can then install Nginx on your Raspberry Pi and host it from there. In the process, you'll be able to pick up a bit of knowledge about Webpack and ECMAScript 2015 (using Babel). Our initial implementation will have only two widgets, a clock and a feed, but those should show you enough of the basics that you should then be able to build other widgets you may have in mind.

Installing our dependencies

First, let's create our package.json:

$ npm init -y

Then install the dependencies:

$ npm install --save-dev babel-cli babel-register babel-core babel-eslint babel-loader babel-preset-es2015 babel-preset-react chai css-loader eslint eslint-loader eslint-plugin-react file-loader istanbul@^1.0.0-alpha.2 jquery jsdom mocha moment node-sass react react-addons-pure-render-mixin react-addons-test-utils react-dom react-hot-loader request sass-loader style-loader url-loader webpack webpack-dev-server

Note that we need to install a specific version of Istanbul to get code coverage.

Next, we create our Webpack config. Save this as webpack.config.js:

1var webpack = require('webpack');
2module.exports = {
3 entry: [
4 'webpack/hot/only-dev-server',
5 "./js/app.js"
6 ],
7 debug: true,
8 devtool: 'source-map',
9 output: {
10 path: __dirname + '/static',
11 filename: "bundle.js"
12 },
13 module: {
14 preLoaders: [
15 {
16 test: /(\.js$|\.jsx$)/,
17 exclude: /node_modules/,
18 loader: "eslint-loader"
19 }
20 ],
21 loaders: [
22 { test: /\.jsx?$/, loaders: ['react-hot', 'babel'], exclude: /node_modules/ },
23 { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader'},
24 { test: /\.woff2?$/, loader: "url-loader?limit=25000" },
25 { test: /\.(eot|svg|ttf)?$/, loader: "file-loader" },
26 { test: /\.scss$/, loader: "style!css!sass" }
27 ]
28 },
29 eslint: {
30 configFile: '.eslintrc.yml'
31 },
32 plugins: [
33 new webpack.HotModuleReplacementPlugin(),
34 new webpack.NoErrorsPlugin()
35 ]
36};

Note the various loaders we're using. We use ESLint to lint our Javascript files for code quality, and the build will fail if they do not match the required standards. We're also using loaders for CSS, Sass, Babel (so we can use ES2015 for our Javascript) and fonts. Also, note the hot module replacement plugin - this allows us to reload the application automatically. If you haven't used Webpack before, this config should be sufficient to get you started, but I recommend reading the documentation.

We also need to configure ESLint how we want. Here is the configuration we will be using, which should be saved as .eslintrc.yml:

1rules:
2 no-debugger:
3 - 0
4 no-console:
5 - 0
6 no-unused-vars:
7 - 0
8 indent:
9 - 2
10 - 2
11 quotes:
12 - 2
13 - single
14 linebreak-style:
15 - 2
16 - unix
17 semi:
18 - 2
19 - always
20env:
21 es6: true
22 browser: true
23 node: true
24extends: 'eslint:recommended'
25parserOptions:
26 sourceType: module
27 ecmaFeatures:
28 jsx: true
29 experimentalObjectRestSpread: true
30 modules: true
31plugins:
32 - react

We also need a base HTML file. Save this as index.html:

1<!doctype html>
2<html lang="en">
3 <head>
4 <meta charset="utf-8">
5 <title>Personal Dashboard</title>
6 </head>
7 <body>
8 <div id="view"></section>
9 <script src="bundle.js"></script>
10 </body>
11</html>

We also need to set the commands for building and testing our app in package.json:

1 "scripts": {
2 "test": "istanbul cover _mocha -- --compilers js:babel-core/register --require ./test/setup.js 'test/**/*.@(js|jsx)'",
3 "test:watch": "npm run test -- --watch",
4 "start": "webpack-dev-server --progress --colors",
5 "build": "webpack --progress --colors"
6 },
7 "babel": {
8 "presets": [
9 "es2015",
10 "react"
11 ]
12 },

The npm test command will call Mocha to run the tests, but will also use Istanbul to generate test coverage. For the sake of brevity, our tests won't be terribly comprehensive. The npm start command will run a development server, while npm run build will build our application.

We also need to create the test/ folder and the test/setup.js file:

1import jsdom from 'jsdom';
2import chai from 'chai';
3
4const doc = jsdom.jsdom('<!doctype html><html><body></body></html>');
5const win = doc.defaultView;
6
7global.document = doc;
8global.window = win;
9
10Object.keys(window).forEach((key) => {
11 if (!(key in global)) {
12 global[key] = window[key];
13 }
14});

This sets up Chai and creates a dummy DOM for our tests. We also need to create the folder js/ and the file js/app.js. You can leave that file empty for now.

If you now run npm start and navigate to http://localhost:8080/webpack-dev-server/, you can see the current state of the application.

Our dashboard component

Our first React component will be a wrapper for all the other ones. Each of the rest of the components will be a self-contained widget that will populate itself without the need for a centralised data store like Redux. I will mention that Redux is a very useful library, and for larger React applications it makes a lot of sense to use it, but here we're better off having each widget manage its own data internally, rather than have it be passed down from a single data store.

Save the following as test/components/dashboard.js:

1import TestUtils from 'react-addons-test-utils';
2import React from 'react';
3import {findDOMNode} from 'react-dom';
4import Dashboard from '../../js/components/dashboard';
5import {expect} from 'chai';
6
7const {renderIntoDocument, scryRenderedDOMComponentsWithClass, Simulate} = TestUtils;
8
9describe('Dashboard', () => {
10 it('renders the dashboard', () => {
11 const component = renderIntoDocument(
12 <Dashboard title="My Dashboard" />
13 );
14 const title = findDOMNode(component.refs.title);
15 expect(title).to.be.ok;
16 expect(title.textContent).to.contain('My Dashboard');
17 });
18}

This tests that we can set the title of our dashboard component. Let's run our tests:

1$ npm test
2
3> personal-dashboard@1.0.0 test /home/matthew/Projects/personal-dashboard
4> istanbul cover _mocha -- --compilers js:babel-core/register --require ./test/setup.js 'test/**/*.@(js|jsx)'
5
6No coverage information was collected, exit without writing coverage information
7module.js:327
8 throw err;
9 ^
10
11Error: Cannot find module '../../js/components/dashboard'
12 at Function.Module._resolveFilename (module.js:325:15)
13 at Function.Module._load (module.js:276:25)
14 at Module.require (module.js:353:17)
15 at require (internal/module.js:12:17)
16 at Object.<anonymous> (dashboard.js:4:1)
17 at Module._compile (module.js:409:26)
18 at loader (/home/matthew/Projects/personal-dashboard/node_modules/babel-register/lib/node.js:148:5)
19 at Object.require.extensions.(anonymous function) [as .js] (/home/matthew/Projects/personal-dashboard/node_modules/babel-register/lib/node.js:158:7)
20 at Module.load (module.js:343:32)
21 at Function.Module._load (module.js:300:12)
22 at Module.require (module.js:353:17)
23 at require (internal/module.js:12:17)
24 at /home/matthew/Projects/personal-dashboard/node_modules/mocha/lib/mocha.js:220:27
25 at Array.forEach (native)
26 at Mocha.loadFiles (/home/matthew/Projects/personal-dashboard/node_modules/mocha/lib/mocha.js:217:14)
27 at Mocha.run (/home/matthew/Projects/personal-dashboard/node_modules/mocha/lib/mocha.js:485:10)
28 at Object.<anonymous> (/home/matthew/Projects/personal-dashboard/node_modules/mocha/bin/_mocha:403:18)
29 at Module._compile (module.js:409:26)
30 at Object.Module._extensions..js (module.js:416:10)
31 at Object.Module._extensions.(anonymous function) (/home/matthew/Projects/personal-dashboard/node_modules/istanbul/lib/hook.js:109:37)
32 at Module.load (module.js:343:32)
33 at Function.Module._load (module.js:300:12)
34 at Function.Module.runMain (module.js:441:10)
35 at runFn (/home/matthew/Projects/personal-dashboard/node_modules/istanbul/lib/command/common/run-with-cover.js:122:16)
36 at /home/matthew/Projects/personal-dashboard/node_modules/istanbul/lib/command/common/run-with-cover.js:251:17
37 at /home/matthew/Projects/personal-dashboard/node_modules/istanbul/lib/util/file-matcher.js:68:16
38 at /home/matthew/Projects/personal-dashboard/node_modules/async/lib/async.js:52:16
39 at /home/matthew/Projects/personal-dashboard/node_modules/async/lib/async.js:361:13
40 at /home/matthew/Projects/personal-dashboard/node_modules/async/lib/async.js:52:16
41 at done (/home/matthew/Projects/personal-dashboard/node_modules/async/lib/async.js:246:17)
42 at /home/matthew/Projects/personal-dashboard/node_modules/async/lib/async.js:44:16
43 at /home/matthew/Projects/personal-dashboard/node_modules/async/lib/async.js:358:17
44 at LOOP (fs.js:1530:14)
45 at nextTickCallbackWith0Args (node.js:420:9)
46 at process._tickCallback (node.js:349:13)
47npm ERR! Test failed. See above for more details.

Our dashboard file doesn't exist. So let's create it:

1$ mkdir js/components
2$ touch js/components/dashboard.js

And run our test again:

1$ npm test
2
3> personal-dashboard@1.0.0 test /home/matthew/Projects/personal-dashboard
4> istanbul cover _mocha -- --compilers js:babel-core/register --require ./test/setup.js 'test/**/*.@(js|jsx)'
5
6
7
8 Dashboard
9Warning: React.createElement: type should not be null, undefined, boolean, or number. It should be a string (for DOM elements) or a ReactClass (for composite components).
10 1) renders the dashboard
11
12
13 0 passing (31ms)
14 1 failing
15
16 1) Dashboard renders the dashboard:
17 Invariant Violation: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.
18 at invariant (node_modules/fbjs/lib/invariant.js:38:15)
19 at [object Object].instantiateReactComponent [as _instantiateReactComponent] (node_modules/react/lib/instantiateReactComponent.js:86:134)
20 at [object Object].ReactCompositeComponentMixin.performInitialMount (node_modules/react/lib/ReactCompositeComponent.js:388:22)
21 at [object Object].ReactCompositeComponentMixin.mountComponent (node_modules/react/lib/ReactCompositeComponent.js:262:21)
22 at Object.ReactReconciler.mountComponent (node_modules/react/lib/ReactReconciler.js:47:35)
23 at mountComponentIntoNode (node_modules/react/lib/ReactMount.js:105:32)
24 at ReactReconcileTransaction.Mixin.perform (node_modules/react/lib/Transaction.js:138:20)
25 at batchedMountComponentIntoNode (node_modules/react/lib/ReactMount.js:126:15)
26 at ReactDefaultBatchingStrategyTransaction.Mixin.perform (node_modules/react/lib/Transaction.js:138:20)
27 at Object.ReactDefaultBatchingStrategy.batchedUpdates (node_modules/react/lib/ReactDefaultBatchingStrategy.js:63:19)
28 at Object.batchedUpdates (node_modules/react/lib/ReactUpdates.js:98:20)
29 at Object.ReactMount._renderNewRootComponent (node_modules/react/lib/ReactMount.js:285:18)
30 at Object.ReactMount._renderSubtreeIntoContainer (node_modules/react/lib/ReactMount.js:371:32)
31 at Object.ReactMount.render (node_modules/react/lib/ReactMount.js:392:23)
32 at ReactTestUtils.renderIntoDocument (node_modules/react/lib/ReactTestUtils.js:85:21)
33 at Context.<anonymous> (dashboard.js:11:23)
34
35
36
37No coverage information was collected, exit without writing coverage information
38npm ERR! Test failed. See above for more details.

Now we have a failing test, we can create our component. Save this as js/components/dashboard.js:

1import React from 'react';
2
3export default React.createClass({
4 render() {
5 return (
6 <div className="dashboard">
7 <h1 ref="title">{this.props.title}</h1>
8 <div className="wrapper">
9 </div>
10 </div>
11 );
12 }
13});

And let's run our tests again:

1$ npm test
2
3> personal-dashboard@1.0.0 test /home/matthew/Projects/personal-dashboard
4> istanbul cover _mocha -- --compilers js:babel-core/register --require ./test/setup.js 'test/**/*.@(js|jsx)'
5
6
7
8 Dashboard
9 ✓ renders the dashboard
10
11
12 1 passing (50ms)
13
14No coverage information was collected, exit without writing coverage information

Our first component is in place. However, it isn't getting loaded. We also need to start thinking about styling. Create the file scss/style.scss, but leave it blank for now. Then save this in js/app.js:

1import React from 'react';
2import ReactDOM from 'react-dom';
3import Dashboard from './components/dashboard';
4import styles from '../scss/style.scss';
5
6ReactDOM.render(
7 <Dashboard title="My Dashboard" />,
8 document.getElementById('view')
9);

Note that we're importing CSS or Sass files in the same way as Javascript files. This is unique to Webpack, and while it takes a bit of getting used to, it has its advantages - if you import only the styles relating to each component, you can be sure there's no orphaned CSS files. Here, we only have one CSS file anyway, so it's a non-issue.

If you now run npm start, our dashboard gets loaded and the title is displayed. With our dashboard in place, we can now implement our first widget.

Creating the clock widget

Our first widget will be a simple clock. This demonstrates changing the state of the widget on an interval. First let's write a test - save this as test/components/clockwidget.js:

1import TestUtils from 'react-addons-test-utils';
2import React from 'react';
3import {findDOMNode} from 'react-dom';
4import ClockWidget from '../../js/components/clockwidget';
5import {expect} from 'chai';
6
7const {renderIntoDocument, scryRenderedDOMComponentsWithClass, Simulate} = TestUtils;
8
9describe('Clock Widget', () => {
10 it('renders the clock widget', () => {
11 const currentTime = 1465160300530;
12 const component = renderIntoDocument(
13 <ClockWidget time={currentTime} />
14 );
15 const time = findDOMNode(component.refs.time);
16 expect(time).to.be.ok;
17 expect(time.textContent).to.contain('Sunday');
18 });
19});

And create an empty file at js/components/clockwidget.js. Then we run our tests again:

1$ npm test
2
3> personal-dashboard@1.0.0 test /home/matthew/Projects/personal-dashboard
4> istanbul cover _mocha -- --compilers js:babel-core/register --require ./test/setup.js 'test/**/*.@(js|jsx)'
5
6
7
8 Clock Widget
9Warning: React.createElement: type should not be null, undefined, boolean, or number. It should be a string (for DOM elements) or a ReactClass (for composite components).
10 1) renders the clock widget
11
12 Dashboard
13 ✓ renders the dashboard
14
15
16 1 passing (46ms)
17 1 failing
18
19 1) Clock Widget renders the clock widget:
20 Invariant Violation: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.
21 at invariant (node_modules/fbjs/lib/invariant.js:38:15)
22 at [object Object].instantiateReactComponent [as _instantiateReactComponent] (node_modules/react/lib/instantiateReactComponent.js:86:134)
23 at [object Object].ReactCompositeComponentMixin.performInitialMount (node_modules/react/lib/ReactCompositeComponent.js:388:22)
24 at [object Object].ReactCompositeComponentMixin.mountComponent (node_modules/react/lib/ReactCompositeComponent.js:262:21)
25 at Object.ReactReconciler.mountComponent (node_modules/react/lib/ReactReconciler.js:47:35)
26 at mountComponentIntoNode (node_modules/react/lib/ReactMount.js:105:32)
27 at ReactReconcileTransaction.Mixin.perform (node_modules/react/lib/Transaction.js:138:20)
28 at batchedMountComponentIntoNode (node_modules/react/lib/ReactMount.js:126:15)
29 at ReactDefaultBatchingStrategyTransaction.Mixin.perform (node_modules/react/lib/Transaction.js:138:20)
30 at Object.ReactDefaultBatchingStrategy.batchedUpdates (node_modules/react/lib/ReactDefaultBatchingStrategy.js:63:19)
31 at Object.batchedUpdates (node_modules/react/lib/ReactUpdates.js:98:20)
32 at Object.ReactMount._renderNewRootComponent (node_modules/react/lib/ReactMount.js:285:18)
33 at Object.ReactMount._renderSubtreeIntoContainer (node_modules/react/lib/ReactMount.js:371:32)
34 at Object.ReactMount.render (node_modules/react/lib/ReactMount.js:392:23)
35 at ReactTestUtils.renderIntoDocument (node_modules/react/lib/ReactTestUtils.js:85:21)
36 at Context.<anonymous> (clockwidget.js:12:23)
37
38
39
40No coverage information was collected, exit without writing coverage information
41npm ERR! Test failed. See above for more details.

With a failing test in place, we can create our component:

1import React from 'react';
2import moment from 'moment';
3
4export default React.createClass({
5 getInitialState() {
6 return {
7 time: this.props.time || moment()
8 };
9 },
10 render() {
11 const time = moment(this.state.time).format('dddd, Do MMMM YYYY, h:mm:ss a');
12 return (
13 <div className="clockwidget widget">
14 <div className="widget-content">
15 <h2 ref="time">{time}</h2>
16 </div>
17 </div>
18 );
19 }
20});

Note that the component accepts a property of time. The getInitialState() method then converts this.props.time into this.state.time so that it can be displayed on render. Note we also set a default of the current time using Moment.js.

We also need to update the dashboard component to load this new component:

1import React from 'react';
2import ClockWidget from './clockwidget';
3
4export default React.createClass({
5 render() {
6 return (
7 <div className="dashboard">
8 <h1 ref="title">{this.props.title}</h1>
9 <div className="wrapper">
10 <ClockWidget />
11 </div>
12 </div>
13 );
14 }
15});

Now, if you try running npm start and viewing the dashboard in the browser, you will see that it displays the current time and date, but it's not being updated. You can force the page to reload every now and then, but we can do better than that. We can set an interval in which the time will refresh. As the smallest unit we show is seconds, this interval should be 1 second.

Amend the clock component as follows:

1import React from 'react';
2import moment from 'moment';
3
4export default React.createClass({
5 getInitialState() {
6 return {
7 time: this.props.time || moment()
8 };
9 },
10 tick() {
11 this.setState({
12 time: moment()
13 });
14 },
15 componentDidMount() {
16 this.interval = setInterval(this.tick, 1000);
17 },
18 componentWillUnmount() {
19 clearInterval(this.interval);
20 },
21 render() {
22 const time = moment(this.state.time).format('dddd, Do MMMM YYYY, h:mm:ss a');
23 return (
24 <div className="clockwidget widget">
25 <div className="widget-content">
26 <h2 ref="time">{time}</h2>
27 </div>
28 </div>
29 );
30 }
31});

When our component has mounted, we set an interval of 1,000 milliseconds, and each time it elapses we call the tick() method. This method sets the state to the current time, and as a result the user interface is automatically re-rendered. On unmount, we clear the interval.

In this case we're just calling a single function on a set interval. In principle, the same approach can be used to populate components in other ways, such as by making an AJAX request.

Creating an RSS widget

Our next widget will be a simple RSS feed reader. We'll fetch the content with jQuery and render it using React. We'll also reload it regularly. First, let's create our test:

1import TestUtils from 'react-addons-test-utils';
2import React from 'react';
3import {findDOMNode} from 'react-dom';
4import FeedWidget from '../../js/components/feedwidget';
5import {expect} from 'chai';
6
7const {renderIntoDocument, scryRenderedDOMComponentsWithClass, Simulate} = TestUtils;
8
9describe('Feed Widget', () => {
10 it('renders the Feed widget', () => {
11 const url = "http://feeds.bbci.co.uk/news/rss.xml?edition=uk"
12 const component = renderIntoDocument(
13 <FeedWidget feed={url} size={5} delay={60} />
14 );
15 const feed = findDOMNode(component.refs.feed);
16 expect(feed).to.be.ok;
17 expect(feed.textContent).to.contain(url);
18 });
19});

Our feed widget will accept an external URL as an argument, and will then poll this URL regularly to populate the feed. It also allows us to specify the size attribute, which denotes the number of feed items, and the delay attribute, which denotes the number of seconds it should wait before fetching the data again.

We also need to amend the dashboard component to include this widget:

1import React from 'react';
2import ClockWidget from './clockwidget';
3import FeedWidget from './feedwidget';
4
5export default React.createClass({
6 render() {
7 return (
8 <div className="dashboard">
9 <h1 ref="title">{this.props.title}</h1>
10 <div className="wrapper">
11 <ClockWidget />
12 <FeedWidget feed="http://feeds.bbci.co.uk/news/rss.xml?edition=uk" size="5" delay="60" />
13 </div>
14 </div>
15 );
16 }
17});

If we then create js/components/feedwidget.js and run npm test:

1$ npm test
2
3> personal-dashboard@1.0.0 test /home/matthew/Projects/personal-dashboard
4> istanbul cover _mocha -- --compilers js:babel-core/register --require ./test/setup.js 'test/**/*.@(js|jsx)'
5
6
7
8 Clock Widget
9 ✓ renders the clock widget (92ms)
10
11 Dashboard
12Warning: React.createElement: type should not be null, undefined, boolean, or number. It should be a string (for DOM elements) or a ReactClass (for composite components). Check the render method of `dashboard`.
13 1) renders the dashboard
14
15 Feed Widget
16Warning: React.createElement: type should not be null, undefined, boolean, or number. It should be a string (for DOM elements) or a ReactClass (for composite components).
17 2) renders the Feed widget
18
19
20 1 passing (286ms)
21 2 failing
22
23 1) Dashboard renders the dashboard:
24 Invariant Violation: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object. Check the render method of `dashboard`.
25 at invariant (node_modules/fbjs/lib/invariant.js:38:15)
26 at instantiateReactComponent (node_modules/react/lib/instantiateReactComponent.js:86:134)
27 at instantiateChild (node_modules/react/lib/ReactChildReconciler.js:43:28)
28 at node_modules/react/lib/ReactChildReconciler.js:70:16
29 at traverseAllChildrenImpl (node_modules/react/lib/traverseAllChildren.js:69:5)
30 at traverseAllChildrenImpl (node_modules/react/lib/traverseAllChildren.js:85:23)
31 at traverseAllChildren (node_modules/react/lib/traverseAllChildren.js:164:10)
32 at Object.ReactChildReconciler.instantiateChildren (node_modules/react/lib/ReactChildReconciler.js:69:7)
33 at ReactDOMComponent.ReactMultiChild.Mixin._reconcilerInstantiateChildren (node_modules/react/lib/ReactMultiChild.js:194:41)
34 at ReactDOMComponent.ReactMultiChild.Mixin.mountChildren (node_modules/react/lib/ReactMultiChild.js:231:27)
35 at ReactDOMComponent.Mixin._createInitialChildren (node_modules/react/lib/ReactDOMComponent.js:715:32)
36 at ReactDOMComponent.Mixin.mountComponent (node_modules/react/lib/ReactDOMComponent.js:531:12)
37 at Object.ReactReconciler.mountComponent (node_modules/react/lib/ReactReconciler.js:47:35)
38 at ReactDOMComponent.ReactMultiChild.Mixin.mountChildren (node_modules/react/lib/ReactMultiChild.js:242:44)
39 at ReactDOMComponent.Mixin._createInitialChildren (node_modules/react/lib/ReactDOMComponent.js:715:32)
40 at ReactDOMComponent.Mixin.mountComponent (node_modules/react/lib/ReactDOMComponent.js:531:12)
41 at Object.ReactReconciler.mountComponent (node_modules/react/lib/ReactReconciler.js:47:35)
42 at [object Object].ReactCompositeComponentMixin.performInitialMount (node_modules/react/lib/ReactCompositeComponent.js:397:34)
43 at [object Object].ReactCompositeComponentMixin.mountComponent (node_modules/react/lib/ReactCompositeComponent.js:262:21)
44 at Object.ReactReconciler.mountComponent (node_modules/react/lib/ReactReconciler.js:47:35)
45 at [object Object].ReactCompositeComponentMixin.performInitialMount (node_modules/react/lib/ReactCompositeComponent.js:397:34)
46 at [object Object].ReactCompositeComponentMixin.mountComponent (node_modules/react/lib/ReactCompositeComponent.js:262:21)
47 at Object.ReactReconciler.mountComponent (node_modules/react/lib/ReactReconciler.js:47:35)
48 at mountComponentIntoNode (node_modules/react/lib/ReactMount.js:105:32)
49 at ReactReconcileTransaction.Mixin.perform (node_modules/react/lib/Transaction.js:138:20)
50 at batchedMountComponentIntoNode (node_modules/react/lib/ReactMount.js:126:15)
51 at ReactDefaultBatchingStrategyTransaction.Mixin.perform (node_modules/react/lib/Transaction.js:138:20)
52 at Object.ReactDefaultBatchingStrategy.batchedUpdates (node_modules/react/lib/ReactDefaultBatchingStrategy.js:63:19)
53 at Object.batchedUpdates (node_modules/react/lib/ReactUpdates.js:98:20)
54 at Object.ReactMount._renderNewRootComponent (node_modules/react/lib/ReactMount.js:285:18)
55 at Object.ReactMount._renderSubtreeIntoContainer (node_modules/react/lib/ReactMount.js:371:32)
56 at Object.ReactMount.render (node_modules/react/lib/ReactMount.js:392:23)
57 at ReactTestUtils.renderIntoDocument (node_modules/react/lib/ReactTestUtils.js:85:21)
58 at Context.<anonymous> (dashboard.js:11:23)
59
60 2) Feed Widget renders the Feed widget:
61 Invariant Violation: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.
62 at invariant (node_modules/fbjs/lib/invariant.js:38:15)
63 at [object Object].instantiateReactComponent [as _instantiateReactComponent] (node_modules/react/lib/instantiateReactComponent.js:86:134)
64 at [object Object].ReactCompositeComponentMixin.performInitialMount (node_modules/react/lib/ReactCompositeComponent.js:388:22)
65 at [object Object].ReactCompositeComponentMixin.mountComponent (node_modules/react/lib/ReactCompositeComponent.js:262:21)
66 at Object.ReactReconciler.mountComponent (node_modules/react/lib/ReactReconciler.js:47:35)
67 at mountComponentIntoNode (node_modules/react/lib/ReactMount.js:105:32)
68 at ReactReconcileTransaction.Mixin.perform (node_modules/react/lib/Transaction.js:138:20)
69 at batchedMountComponentIntoNode (node_modules/react/lib/ReactMount.js:126:15)
70 at ReactDefaultBatchingStrategyTransaction.Mixin.perform (node_modules/react/lib/Transaction.js:138:20)
71 at Object.ReactDefaultBatchingStrategy.batchedUpdates (node_modules/react/lib/ReactDefaultBatchingStrategy.js:63:19)
72 at Object.batchedUpdates (node_modules/react/lib/ReactUpdates.js:98:20)
73 at Object.ReactMount._renderNewRootComponent (node_modules/react/lib/ReactMount.js:285:18)
74 at Object.ReactMount._renderSubtreeIntoContainer (node_modules/react/lib/ReactMount.js:371:32)
75 at Object.ReactMount.render (node_modules/react/lib/ReactMount.js:392:23)
76 at ReactTestUtils.renderIntoDocument (node_modules/react/lib/ReactTestUtils.js:85:21)
77 at Context.<anonymous> (feedwidget.js:12:23)
78
79
80
81
82=============================== Coverage summary ===============================
83Statements : 83.33% ( 10/12 )
84Branches : 50% ( 1/2 )
85Functions : 66.67% ( 4/6 )
86Lines : 83.33% ( 10/12 )
87================================================================================
88npm ERR! Test failed. See above for more details.

Our test fails, so we can start work on the widget proper. Here it is:

1import React from 'react';
2import jQuery from 'jquery';
3window.jQuery = jQuery;
4
5const FeedItem = React.createClass({
6 render() {
7 return (
8 <a href={this.props.link} target="_blank">
9 <li className="feeditem">{this.props.title}</li>
10 </a>
11 );
12 }
13});
14
15export default React.createClass({
16 getInitialState() {
17 return {
18 feed: [],
19 size: this.props.size || 5
20 };
21 },
22 componentDidMount() {
23 this.getFeed();
24 this.interval = setInterval(this.getFeed, (this.props.delay * 1000));
25 },
26 componentWillUnmount() {
27 clearInterval(this.interval);
28 },
29 getFeed() {
30 let that = this;
31 jQuery.ajax({
32 url: this.props.feed,
33 success: function (response) {
34 let xml = jQuery(response);
35 let feed = [];
36 xml.find('item').each(function () {
37 let item = {};
38 item.title = jQuery(this).find('title').text();
39 item.link = jQuery(this).find('guid').text();
40 feed.push(item);
41 });
42 that.setState({
43 feed: feed.slice(0,that.state.size)
44 });
45 }
46 });
47 },
48 render() {
49 let feedItems = this.state.feed.map(function (item, index) {
50 return (
51 <FeedItem title={item.title} link={item.link} key={item.link}></FeedItem>
52 );
53 });
54 return (
55 <div className="feedwidget widget">
56 <div className="widget-content">
57 <h2 ref="feed"> Fetched from {this.props.feed}</h2>
58 <ul>
59 {feedItems}
60 </ul>
61 </div>
62 </div>
63 );
64 }
65});

This is by far the most complex component, so a little explanation is called for. We include jQuery as a dependency at the top of the file. Then we create a component for rendering an individual feed item, called FeedItem. This is very simple, consisting of an anchor tag wrapped around a list item. Note the use of the const keyword - in ES6 this denotes a constant.

Next, we move onto the feed widget proper. We set the initial state of the feed to be an empty array. Then, we define a componentDidMount() method that calls getFeed() and sets up an interval to call it again, based on the delay property. The getFeed() method fetches the URL in question and sets this.state.feed to an array of the most recent entries in the feed, with the size denoted by the size property passed through. We also clear that interval when the component is about to be unmounted.

Note that you may have problems with the Access-Control-Allow-Origin HTTP header. It's possible to disable this in your web browser, so if you want to run this as a dashboard you'll probably need to do so. On Chrome there's a useful plugin that allows you to disable this when needed.

Because our FeedWidget has been created in a generic manner, we can then include multiple feed widgets easily, as in this example:

1import React from 'react';
2import ClockWidget from './clockwidget';
3import FeedWidget from './feedwidget';
4
5export default React.createClass({
6 render() {
7 return (
8 <div className="dashboard">
9 <h1 ref="title">{this.props.title}</h1>
10 <div className="wrapper">
11 <ClockWidget />
12 <FeedWidget feed="http://feeds.bbci.co.uk/news/rss.xml?edition=uk" size="5" delay="60" />
13 <FeedWidget feed="https://www.sitepoint.com/feed/" size="10" delay="120" />
14 </div>
15 </div>
16 );
17 }
18});

We also need to style our widgets. Save this as scss/_colours.scss:

1$bgColour: #151515;
2$txtColour: #cfcfcf;
3$clockBg: #fa8c00;
4$clockHoverBg: #0099ff;
5$clockTxt: #fff;
6$feedBg: #0099ff;
7$feedTxt: #fff;
8$feedHoverBg: #fa8c00;

And this as scss/style.scss:

1@import 'colours';
2
3html, body {
4 background-color: $bgColour;
5 color: $txtColour;
6 font-family: Arial, Helvetica, sans-serif;
7}
8
9div.dashboard {
10 padding: 10px;
11}
12
13div.wrapper {
14 -moz-column-count: 4;
15 -webkit-column-count: 4;
16 column-count: 4;
17 -moz-column-gap: 1em;
18 -webkit-column-gap: 1em;
19 column-gap: 1em;
20}
21
22div.widget {
23 display: inline-block;
24 margin: 0 0 1em;
25 width: 100%;
26 min-height: 100px;
27 margin: 5px;
28 opacity: 0.8;
29 transition: opacity 1s;
30
31 &:hover {
32 opacity: 1;
33 }
34
35 h2, h4 {
36 padding: 20px;
37 }
38
39 div.widget-content {
40 width: 100%;
41 }
42}
43
44div.clockwidget {
45 background-color: $clockBg;
46 color: $clockTxt;
47}
48
49div.feedwidget {
50 background-color: $feedBg;
51 color: $feedTxt;
52
53 h2 {
54 word-wrap: break-word;
55 }
56
57 ul {
58 margin-left: 0;
59 padding-left: 20px;
60
61 a {
62 text-decoration: none;
63 padding: 5px;
64
65 li {
66 list-style-type: none;
67 font-weight: bold;
68 color: $feedTxt;
69 }
70 }
71 }
72}

The end result should look something like this:

The personal dashboard in action

With that done, feel free to add whatever other feeds you want to include.

Deploying our dashboard

The final step is deploying our dashboard to our Raspberry Pi or other device. Run the following command to generate the Javascript:

$ npm run build

This will create static/bundle.js. You can then copy that file over to your web server with index.html and place both files in the web root. I recommend using Nginx if you're using a Raspberry Pi as it's faster and simpler for static content. If you're likely to make a lot of changes you might want to create a command in the scripts section of your package.json to deploy the files more easily.

These basic widgets should be enough to get you started. You should be able to use the feed widget with virtually any RSS feed, and you should be able to use a similar approach to poll third-party APIs, although you might need to authenticate in some way (if you do, you won't want to expose your authentication details, so ensure that nobody from outside the network can view your application). I'll leave it to you to see what kind of interesting widgets you come up with for your own dashboard, but some ideas to get you started include:

  • Public transport schedules/Traffic issues
  • Weather reports
  • Shopping lists/Todo lists, with HTML5 local storage used to persist them
  • Galleries of recent photos on social networks
  • Status of servers on cloud hosting providers

With a little thought, you can probably come up with a few more than that! I've created a Github repository with the source code so you can check your own implementation against it.