Creating a personal dashboard with React and Webpack
Published by Matthew Daly 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 - 04 no-console:5 - 06 no-unused-vars:7 - 08 indent:9 - 210 - 211 quotes:12 - 213 - single14 linebreak-style:15 - 216 - unix17 semi:18 - 219 - always20env:21 es6: true22 browser: true23 node: true24extends: 'eslint:recommended'25parserOptions:26 sourceType: module27 ecmaFeatures:28 jsx: true29 experimentalObjectRestSpread: true30 modules: true31plugins: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';34const doc = jsdom.jsdom('<!doctype html><html><body></body></html>');5const win = doc.defaultView;67global.document = doc;8global.window = win;910Object.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';67const {renderIntoDocument, scryRenderedDOMComponentsWithClass, Simulate} = TestUtils;89describe('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 test23> personal-dashboard@1.0.0 test /home/matthew/Projects/personal-dashboard4> istanbul cover _mocha -- --compilers js:babel-core/register --require ./test/setup.js 'test/**/*.@(js|jsx)'56No coverage information was collected, exit without writing coverage information7module.js:3278 throw err;9 ^1011Error: 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:2725 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:1737 at /home/matthew/Projects/personal-dashboard/node_modules/istanbul/lib/util/file-matcher.js:68:1638 at /home/matthew/Projects/personal-dashboard/node_modules/async/lib/async.js:52:1639 at /home/matthew/Projects/personal-dashboard/node_modules/async/lib/async.js:361:1340 at /home/matthew/Projects/personal-dashboard/node_modules/async/lib/async.js:52:1641 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:1643 at /home/matthew/Projects/personal-dashboard/node_modules/async/lib/async.js:358:1744 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/components2$ touch js/components/dashboard.js
And run our test again:
1$ npm test23> personal-dashboard@1.0.0 test /home/matthew/Projects/personal-dashboard4> istanbul cover _mocha -- --compilers js:babel-core/register --require ./test/setup.js 'test/**/*.@(js|jsx)'5678 Dashboard9Warning: 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 dashboard111213 0 passing (31ms)14 1 failing1516 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)34353637No coverage information was collected, exit without writing coverage information38npm 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';23export 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 test23> personal-dashboard@1.0.0 test /home/matthew/Projects/personal-dashboard4> istanbul cover _mocha -- --compilers js:babel-core/register --require ./test/setup.js 'test/**/*.@(js|jsx)'5678 Dashboard9 ✓ renders the dashboard101112 1 passing (50ms)1314No 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';56ReactDOM.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';67const {renderIntoDocument, scryRenderedDOMComponentsWithClass, Simulate} = TestUtils;89describe('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 test23> personal-dashboard@1.0.0 test /home/matthew/Projects/personal-dashboard4> istanbul cover _mocha -- --compilers js:babel-core/register --require ./test/setup.js 'test/**/*.@(js|jsx)'5678 Clock Widget9Warning: 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 widget1112 Dashboard13 ✓ renders the dashboard141516 1 passing (46ms)17 1 failing1819 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)37383940No coverage information was collected, exit without writing coverage information41npm 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';34export 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';34export 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';34export 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';67const {renderIntoDocument, scryRenderedDOMComponentsWithClass, Simulate} = TestUtils;89describe('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';45export 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 test23> personal-dashboard@1.0.0 test /home/matthew/Projects/personal-dashboard4> istanbul cover _mocha -- --compilers js:babel-core/register --require ./test/setup.js 'test/**/*.@(js|jsx)'5678 Clock Widget9 ✓ renders the clock widget (92ms)1011 Dashboard12Warning: 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 dashboard1415 Feed Widget16Warning: 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 widget181920 1 passing (286ms)21 2 failing2223 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:1629 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)5960 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)7879808182=============================== 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;45const 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});1415export default React.createClass({16 getInitialState() {17 return {18 feed: [],19 size: this.props.size || 520 };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';45export 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';23html, body {4 background-color: $bgColour;5 color: $txtColour;6 font-family: Arial, Helvetica, sans-serif;7}89div.dashboard {10 padding: 10px;11}1213div.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}2122div.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;3031 &:hover {32 opacity: 1;33 }3435 h2, h4 {36 padding: 20px;37 }3839 div.widget-content {40 width: 100%;41 }42}4344div.clockwidget {45 background-color: $clockBg;46 color: $clockTxt;47}4849div.feedwidget {50 background-color: $feedBg;51 color: $feedTxt;5253 h2 {54 word-wrap: break-word;55 }5657 ul {58 margin-left: 0;59 padding-left: 20px;6061 a {62 text-decoration: none;63 padding: 5px;6465 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:
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.