React is a library for creating UIs.
Two huge benefits:
Components
UI Composed of divs, spans, inputs and others.
React allows us to build our own components and use them as built in HTML elements.
Tested once, reused everywhere.
Declarative
This means our program describes what we are doing.
JS is normally imperative, whereas something like HTML is declarative.
Normally, we would have a data model with JS and then the view made up with HTML. This is hard to keep in sync.
Example, if the second item in the data model is removed, then we would also need to remove a second list item in HTML.
React makes an efficient and fast way to "remove a DOM and reload". This is done by using a Virtual DOM
We describe the application entirely in the .js files.
The JS representation of the DOM is referred to as the Virtual DOM. This is cheap and fast to use.
Interacting with the DOM is a lot slower than manipulating JavaScript.
When we write our React markup code, React takes on the responsibility of rendering the real DOM element on the Virtual DOM we define.
It's smart to remember the previous and current DOM and makes the minimum changes needed.
DOM Elements have:
To describe an <a>
tag, we use all three:
React.createElement( 'a', { href: 'https://abc.com' }, 'abc' );
However, it is not convenient to do this call. This is where JSX comes in.
So the above is equivalent to...
const myLink = <a href="https://abc.com">abc</a>;
The compiler that we change JSX to what we need is Babel.
app.css app.jsx index.html
index.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Scoreboard</title> <link rel="stylesheet" href="./app.css" /> </head> <body> <div id="container">Loading...</div> <script src="./vendor/react.js"></script> <script src="./vendor/react-dom.js"></script> <script src="./vendor/babel-browser.min.js"></script> <script type="text/babel" src="./app.jsx"></script> </body> </html>
app.jsx
function Application() { return ( <div> <h1>Hello from React</h1> <p>I was rendered from the Application component</p> </div> ); } ReactDOM.render(<h1>Hello</h1>, document.getElementById('container'));
Really, we want to create a component to render instead of individual components.
Components need to start with a capital as convention.
A react component must only return one DOM element.
app.jsx
function Application() { return ( <div> <h1>Hello from React</h1> <p>I was rendered from the Application component</p> </div> ); } ReactDOM.render(<Application />, document.getElementById('container'));
It is a lot easier to debug with the React DevTools extension.
Download React Developer Tools. Available on both Chrome and Firefox.
This represents the virtual DOM.
React also highlights what is being inspected if you click on the React tab after highlighting the section you are looking for.
It's best to mock up the application in JSX.
That way, we can apply some styles now.
function Application() { return ( <div className="scoreboard"> <div className="header"> <h1>Scoreboard</h1> </div> <div className="players"> <div className="player"> <div className="player-name">Dennis</div> <div className="player-score"> <div className="counter"> <button className="counter-action decrement"> - </button> <div className="counter-score"> 31 </div> <button className="counter-action increment"> + </button> </div> </div> </div> </div> </div> ); }
The application so far is pretty useless. We can use properties to customise our components. We can then use the attribute syntax which is used to create the virtual DOM.
function Application(props) { return ( <div className="scoreboard"> <div className="header"> <h1>{props.title}</h1> </div> <div className="players"> <div className="player"> <div className="player-name">Dennis</div> <div className="player-score"> <div className="counter"> <button className="counter-action decrement"> - </button> <div className="counter-score"> 31 </div> <button className="counter-action increment"> + </button> </div> </div> </div> </div> </div> ); } ReactDOM.render( <Application title="My Scoreboard" />, document.getElementById('container') );
PropTypes is an object that contains all the keys our object can take and a special type definition.
function Application(props) { return ( <div className="scoreboard"> <div className="header"> <h1>{props.title}</h1> </div> <div className="players"> <div className="player"> <div className="player-name">Dennis</div> <div className="player-score"> <div className="counter"> <button className="counter-action decrement"> - </button> <div className="counter-score"> 31 </div> <button className="counter-action increment"> + </button> </div> </div> </div> </div> </div> ); } Application.propTypes = { title: React.PropTypes.string.isRequired }; Application.defaultProps = { title: 'Scoreboard' }; ReactDOM.render( <Application title="My Scoreboard" />, document.getElementById('container') );
Breaking down our Application into smaller components.
To this is, we think of Application from a high level.
function Header(props) { return ( <div className="header"> <h1>{props.title}</h1> </div> ); } Header.propTypes = { title: React.PropTypes.string.isRequired }; function Counter(props) { return ( <div className="counter"> <button className="counter-action decrement"> - </button> <div className="counter-score"> {props.score}</div> <button className="counter-action increment"> + </button> </div> ); } Counter.propTypes = { score: React.PropTypes.number.isRequired }; function Player(props) { return ( <div className="player"> <div className="player-name">{props.name}</div> <div className="player-score"> <Counter score={props.score} /> </div> </div> ); } Player.propTypes = { name: React.PropTypes.string.isRequired, score: React.PropTypes.number.isRequired }; function Application(props) { return ( <div className="scoreboard"> <Header title={props.title} /> <div className="players"> <Player name="Dennis" score={32} /> <Player name="Ben" score={34} /> </div> </div> ); } Application.propTypes = { title: React.PropTypes.string }; Application.defaultProps = { title: 'Scoreboard' }; ReactDOM.render( <Application title="My Scoreboard" />, document.getElementById('container') );
Currently, we have two hard coded players, but we want to be able to loop through an array of this.
let PLAYERS = [ { name: "Dennis", score: 33, id:1, }, { name: "Ben", score: 34, id:2, }, { name: "Clark From InVision", score: 12, id:3, } ]; function Header(props) { return ( <div className="header"> <h1>{props.title}</h1> </div> ); } Header.propTypes = { title: React.PropTypes.string.isRequired, }; function Counter(props) { return ( <div className="counter"> <button className="counter-action decrement"> - </button> <div className="counter-score"> {props.score }</div> <button className="counter-action increment"> + </button> </div> ); } Counter.propTypes = { score: React.PropTypes.number.isRequired, } function Player(props) { return ( <div className="player"> <div className="player-name"> {props.name} </div> <div className="player-score"> <Counter score={props.score} /> </div> </div> ); } Player.propTypes = { name: React.PropTypes.string.isRequired, score: React.PropTypes.number.isRequired, } function Application(props) { return( <div className="scoreboard"> <Header title={props.title}/> <div className="players"> {props.players.map(function (player) { return <Player name={player.name} score{player.score} key={player.id} /> })} </div> </div> ) } Application.propTypes = { title: React.PropTypes.string, player: React.PropTypes.arrayOf(React.PropTypes.shape({ name: React.PropTypes.string.isRequired, score: React.PropTypes.number.isRequired, id: React.PropTypes.number.isRequired, })).isRequired, }; Application.defaultProps = { title: "Scoreboard", }; ReactDOM.render(<Application players={PLAYERS}/>, document.getElementById('container'));
Right now, our application is still static. We cannot update any data details.
We need to add some State. So far, we have been writing Stateless Function Components (SFC). This form isn't designed for handling state.
We need to build a component class, but this comes with complexity. Now we will refactor to reflect this.
let PLAYERS = [ { name: "Dennis", score: 33, id:1, }, { name: "Ben", score: 34, id:2, }, { name: "Clark From InVision", score: 12, id:3, } ]; function Header(props) { return ( <div className="header"> <h1>{props.title}</h1> </div> ); } Header.propTypes = { title: React.PropTypes.string.isRequired, }; // functionally the same, we are preparing to be able to add state to the component var Counter = React.createClass({ propTypes: { score: React.PropTypes.number.isRequired, }, render: function() { return ( <div className="counter"> <button className="counter-action decrement"> - </button> <div className="counter-score"> {this.props.score }</div> <button className="counter-action increment"> + </button> </div> ); } }); // function Counter(props) {} - this can be removed // Counter.propTypes = {} - this can be removed function Player(props) { return ( <div className="player"> <div className="player-name"> {props.name} </div> <div className="player-score"> <Counter score={props.score} /> </div> </div> ); } Player.propTypes = { name: React.PropTypes.string.isRequired, score: React.PropTypes.number.isRequired, } function Application(props) { return( <div className="scoreboard"> <Header title={props.title}/> <div className="players"> {props.players.map(function (player) { return <Player name={player.name} score{player.score} key={player.id} /> })} </div> </div> ) } Application.propTypes = { title: React.PropTypes.string, player: React.PropTypes.arrayOf(React.PropTypes.shape({ name: React.PropTypes.string.isRequired, score: React.PropTypes.number.isRequired, id: React.PropTypes.number.isRequired, })).isRequired, }; Application.defaultProps = { title: "Scoreboard", }; ReactDOM.render(<Application players={PLAYERS}/>, document.getElementById('container'));
Managing data that can change. React has a mechanism to deal with State. React doesn't provide everything, and you're encouraged to use other libraries to deal with things such as AJAX and State Management.
A popular design pattern used is Flux (what we will use). It is also recommended to use Redux which takes Flux a little bit further, however Flux and Redux are topics for another time.
After understanding state, it is recommended to use these other libraries to save time and work more effectively.
let PLAYERS = [ { name: "Dennis", score: 33, id:1, }, { name: "Ben", score: 34, id:2, }, { name: "Clark From InVision", score: 12, id:3, } ]; function Header(props) { return ( <div className="header"> <h1>{props.title}</h1> </div> ); } Header.propTypes = { title: React.PropTypes.string.isRequired, }; // functionally the same, we are preparing to be able to add state to the component var Counter = React.createClass({ propTypes: { // this is no longer needed // score: React.PropTypes.number.isRequired, initialScore: React.PropTypes.number.isRequired }, getInitialState: function() { return { score: this.props.initialScore, } }, incrementScore: function(e) { // uncomment if you want to check out the event in the console // console.log("increment score", e); this.setState({ score: (this.state.score + 1), }) }, decrementScore: function(e) { this.setState({ score: (this.state.score - 1), }) }, render: function() { return ( <div className="counter"> <button className="counter-action decrement" onClick={this.decrementScore}> - </button> <div className="counter-score"> {this.state.score }</div> <button className="counter-action increment" onClick={this.incrementScore}> + </button> </div> ); } }); // function Counter(props) {} - this can be removed // Counter.propTypes = {} - this can be removed function Player(props) { return ( <div className="player"> <div className="player-name"> {props.name} </div> <div className="player-score"> <Counter initialScore={props.score}/> </div> </div> ); } Player.propTypes = { name: React.PropTypes.string.isRequired, score: React.PropTypes.number.isRequired, } function Application(props) { return( <div className="scoreboard"> <Header title={props.title}/> <div className="players"> {props.players.map(function (player) { return <Player name={player.name} score{player.score} key={player.id} /> })} </div> </div> ) } Application.propTypes = { title: React.PropTypes.string, player: React.PropTypes.arrayOf(React.PropTypes.shape({ name: React.PropTypes.string.isRequired, score: React.PropTypes.number.isRequired, id: React.PropTypes.number.isRequired, })).isRequired, }; Application.defaultProps = { title: "Scoreboard", }; ReactDOM.render(<Application players={PLAYERS}/>, document.getElementById('container'));
It's cumbersome to maintain State when applications scale. We should think of State as the following:
Component state is normally not shared or visible outside of a component.
Application state should be handled as high up as possible. We can pass all the changes down using properties.
Because of the direction of data flow, parents don't call methods to change the values of children. Instead, they pass down new values to declare how the children should be re-rendered. (Back to the whole Declarative programming!)
This Parent passing down to Child is what we call Unidirectional Data Flow. If it changes at the top, it will cascade down the virtual DOM.
Since children cannot talk to parents, we can implement callback functions to update data. When a child wants to indicate a State should change, they will use the callback.
Currently, we have State in a number of places. We will change this to become more unidirectional.
At the moment, we have one State. Our counter component. However, as useful as it has been to show how States work by using counter, the State of the counter is really a state of a Player Score.
We're going to relocate the state up to the Application.
let PLAYERS = [ { name: "Dennis", score: 33, id:1, }, { name: "Ben", score: 34, id:2, }, { name: "Clark From InVision", score: 12, id:3, } ]; function Header(props) { return ( <div className="header"> <h1>{props.title}</h1> </div> ); } Header.propTypes = { title: React.PropTypes.string.isRequired, }; function Counter(props) { return ( <div className="counter"> <button className="counter-action decrement"> - </button> <div className="counter-score"> {props.score }</div> <button className="counter-action increment"> + </button> </div> ); } Counter.propTypes = { score: React.PropTypes.number.isRequired, } function Player(props) { return ( <div className="player"> <div className="player-name"> {props.name} </div> <div className="player-score"> <Counter score={props.score}/> </div> </div> ); } Player.propTypes = { name: React.PropTypes.string.isRequired, score: React.PropTypes.number.isRequired, } // function Application(props) {} - now this is removed // props moved into the class var Application = React.createClass({ propTypes: { title: React.PropTypes.string, initialPlayers: React.PropTypes.arrayOf(React.PropTypes.shape({ name: React.PropTypes.string.isRequired, score: React.PropTypes.number.isRequired, id: React.PropTypes.number.isRequired, })).isRequired, }, getDefaultProps: function() { return { title: "Scoreboard", } }, getInitialState: function() { return { players: this.props.initialPlayers, }; }, render: function() { return( <div className="scoreboard"> <Header title={this.props.title}/> <div className="players"> {this.state.players.map(function (player) { return <Player name={player.name} score{player.score} key={player.id} /> })} </div> </div> ); } }); // this is also removed // Application.defaultProps = { // title: "Scoreboard", // }; ReactDOM.render(<Application intialPlayers={PLAYERS}/>, document.getElementById('container'));
We now need to implement these callback functions so that we can change our counter.
let PLAYERS = [ { name: "Dennis", score: 33, id:1, }, { name: "Ben", score: 34, id:2, }, { name: "Clark From InVision", score: 12, id:3, } ]; function Header(props) { return ( <div className="header"> <h1>{props.title}</h1> </div> ); } Header.propTypes = { title: React.PropTypes.string.isRequired, }; function Counter(props) { return ( <div className="counter"> <button className="counter-action decrement" onClick={function() {props.onChange(-1)}}> - </button> <div className="counter-score"> {props.score }</div> <button className="counter-action increment" onClick={function() {props.onChange(1)}}> + </button> </div> ); } Counter.propTypes = { score: React.PropTypes.number.isRequired, onChange: React.PropTypes.func.isRequired, } function Player(props) { return ( <div className="player"> <div className="player-name"> {props.name} </div> <div className="player-score"> <Counter score={props.score} onChange={props.onScoreChange}/> </div> </div> ); } Player.propTypes = { name: React.PropTypes.string.isRequired, score: React.PropTypes.number.isRequired, onScoreChange: React.PropTypes.func.isRequired, } var Application = React.createClass({ propTypes: { title: React.PropTypes.string, initialPlayers: React.PropTypes.arrayOf(React.PropTypes.shape({ name: React.PropTypes.string.isRequired, score: React.PropTypes.number.isRequired, id: React.PropTypes.number.isRequired, })).isRequired, }, getDefaultProps: function() { return { title: "Scoreboard", } }, getInitialState: function() { return { players: this.props.initialPlayers, }; }, onScoreChange: function(index, delta) { // uncomment this to double check value change on the application // console.log('onScoreChange', index, delta); this.state.players[index].score += delta; this.setState(this.state); }, render: function() { return( <div className="scoreboard"> <Header title={this.props.title}/> <div className="players"> {this.state.players.map(function (player, index) { return ( <Player onScoreChange={function(delta) { this.onScoreChange(index,delta)}.bind(this) } name={player.name} score{player.score} key={player.id} /> ); }.bind(this))} </div> </div> ); } }); ReactDOM.render(<Application intialPlayers={PLAYERS}/>, document.getElementById('container'));
The problem with keeping the score locally is that if wanted to do something (eg show to total score) we wouldn't have had access to that.
Let's show that by creating some stats.
let PLAYERS = [ { name: "Dennis", score: 33, id:1, }, { name: "Ben", score: 34, id:2, }, { name: "Clark From InVision", score: 12, id:3, } ]; function Stats(props) { // you don't have to store it, but it's handy for organisation var totalPlayers = props.players.length; var totalPoints = props.players.reduce(function(total, player){ return total + player.score; }, 0) return ( <table className="stats"> <tbody> <tr> <td>Players:</td> <td>{totalPlayers}</td> </tr> <tr> <td>Total Points:</td> <td>{totalPoints}</td> </tr> </tbody> </table> ); } Stats.propTypes = { players: React.PropTypes.array.isRequired, }; function Header(props) { return ( <div className="header"> <Stats /> <h1>{props.title}</h1> </div> ); } Header.propTypes = { title: React.PropTypes.string.isRequired, }; function Counter(props) { return ( <div className="counter"> <button className="counter-action decrement" onClick={function() {props.onChange(-1)}}> - </button> <div className="counter-score"> {props.score }</div> <button className="counter-action increment" onClick={function() {props.onChange(1)}}> + </button> </div> ); } Counter.propTypes = { score: React.PropTypes.number.isRequired, onChange: React.PropTypes.func.isRequired, } function Player(props) { return ( <div className="player"> <div className="player-name"> {props.name} </div> <div className="player-score"> <Counter score={props.score} onChange={props.onScoreChange}/> </div> </div> ); } Player.propTypes = { name: React.PropTypes.string.isRequired, score: React.PropTypes.number.isRequired, onScoreChange: React.PropTypes.func.isRequired, } var Application = React.createClass({ propTypes: { title: React.PropTypes.string, initialPlayers: React.PropTypes.arrayOf(React.PropTypes.shape({ name: React.PropTypes.string.isRequired, score: React.PropTypes.number.isRequired, id: React.PropTypes.number.isRequired, })).isRequired, }, getDefaultProps: function() { return { title: "Scoreboard", } }, getInitialState: function() { return { players: this.props.initialPlayers, }; }, onScoreChange: function(index, delta) { // uncomment this to double check value change on the application // console.log('onScoreChange', index, delta); this.state.players[index].score += delta; this.setState(this.state); }, render: function() { return( <div className="scoreboard"> <Header title={this.props.title} players={this.state.players} /> <div className="players"> {this.state.players.map(function (player, index) { return ( <Player onScoreChange={function(delta) { this.onScoreChange(index,delta)}.bind(this) } name={player.name} score{player.score} key={player.id} /> ); }.bind(this))} </div> </div> ); } }); ReactDOM.render(<Application intialPlayers={PLAYERS}/>, document.getElementById('container'));
In React, States are our responsibility. What happens if we need to add players?
let PLAYERS = [ { name: "Dennis", score: 33, id:1, }, { name: "Ben", score: 34, id:2, }, { name: "Clark From InVision", score: 12, id:3, } ]; var nextId = 4; var AddPlayerForm = React.createClass({ propTypes: { onAdd: React.PropTypes.func.isRequired, }, getInitialState: function() { return { name: "" }; }, onNameChange: function(e) { // uncomment to see the changes on target value // console.log("onNameChange", e.target.value); this.setState({name: e.target.value}); }, onSubmit: function(e) { e.preventDefault(); this.props.onAdd(this.state.name); this.setState({name: ""}); }, render: function() { return ( <div className="add-player-form"> <form onSubmit={this.onSubmit}> <input type="text" value={this.state.name} onChange={this.onNameChange} /> <input type="submit" value="Add Player" /> </form> </div> ); } }); function Stats(props) { // you don't have to store it, but it's handy for organisation var totalPlayers = props.players.length; var totalPoints = props.players.reduce(function(total, player){ return total + player.score; }, 0) return ( <table className="stats"> <tbody> <tr> <td>Players:</td> <td>{totalPlayers}</td> </tr> <tr> <td>Total Points:</td> <td>{totalPoints}</td> </tr> </tbody> </table> ); } Stats.propTypes = { players: React.PropTypes.array.isRequired, }; function Header(props) { return ( <div className="header"> <Stats /> <h1>{props.title}</h1> </div> ); } Header.propTypes = { title: React.PropTypes.string.isRequired, }; function Counter(props) { return ( <div className="counter"> <button className="counter-action decrement" onClick={function() {props.onChange(-1)}}> - </button> <div className="counter-score"> {props.score }</div> <button className="counter-action increment" onClick={function() {props.onChange(1)}}> + </button> </div> ); } Counter.propTypes = { score: React.PropTypes.number.isRequired, onChange: React.PropTypes.func.isRequired, } function Player(props) { return ( <div className="player"> <div className="player-name"> <a className="remove-player" onClick={props.onRemove}>x</a> {props.name} </div> <div className="player-score"> <Counter score={props.score} onChange={props.onScoreChange}/> </div> </div> ); } Player.propTypes = { name: React.PropTypes.string.isRequired, score: React.PropTypes.number.isRequired, onScoreChange: React.PropTypes.func.isRequired, onRemove: React.PropTypes.func.isRequired, } var Application = React.createClass({ propTypes: { title: React.PropTypes.string, initialPlayers: React.PropTypes.arrayOf(React.PropTypes.shape({ name: React.PropTypes.string.isRequired, score: React.PropTypes.number.isRequired, id: React.PropTypes.number.isRequired, })).isRequired, }, getDefaultProps: function() { return { title: "Scoreboard", } }, getInitialState: function() { return { players: this.props.initialPlayers, }; }, onScoreChange: function(index, delta) { // uncomment this to double check value change on the application // console.log('onScoreChange', index, delta); this.state.players[index].score += delta; this.setState(this.state); }, onPlayerRemove: function(index) { // uncomment to see the player index // console.log('remove', index); this.state.players.splice(index, 1); setState(this.state); }, onPlayerAdd: function() { // uncomment to see new player name //console.log('Player added:', name); this.state.players.push({ name: name, score: 0, id: nextId, }); /* NOTE: in something like Redux, we don't update the state itself, we actually create a brand new state object */ this.setState(this.state); nextId += 1; }; render: function() { return( <div className="scoreboard"> <Header title={this.props.title} players={this.state.players} /> <div className="players"> {this.state.players.map(function (player, index) { return ( <Player onScoreChange={function(delta) { this.onScoreChange(index,delta)}.bind(this) } onRemove={function() { this.onRemovePlayer(index); }.bind(this) } name={player.name} score{player.score} key={player.id} /> ); }.bind(this))} </div> <AddPlayerForm onAdd={this.onPlayerAdd} /> </div> ); } }); ReactDOM.render(<Application intialPlayers={PLAYERS}/>, document.getElementById('container'));
Let's build a more advanced component. A stopwatch.
We will need a timer in seconds. We will have a button to stop and start the timer.
let PLAYERS = [ { name: "Dennis", score: 33, id:1, }, { name: "Ben", score: 34, id:2, }, { name: "Clark From InVision", score: 12, id:3, } ]; var nextId = 4; var StopWatch = React.createClass({ render: function() { return ( <div className="stopwatch"> <h2>Stopwatch</h2> <div className="stopwatch-time">0</div> <button>Start</button> <button>Reset</button> </div> ); } }); var AddPlayerForm = React.createClass({ propTypes: { onAdd: React.PropTypes.func.isRequired, }, getInitialState: function() { return { name: "" }; }, onNameChange: function(e) { // uncomment to see the changes on target value // console.log("onNameChange", e.target.value); this.setState({name: e.target.value}); }, onSubmit: function(e) { e.preventDefault(); this.props.onAdd(this.state.name); this.setState({name: ""}); }, render: function() { return ( <div className="add-player-form"> <form onSubmit={this.onSubmit}> <input type="text" value={this.state.name} onChange={this.onNameChange} /> <input type="submit" value="Add Player" /> </form> </div> ); } }); function Stats(props) { // you don't have to store it, but it's handy for organisation var totalPlayers = props.players.length; var totalPoints = props.players.reduce(function(total, player){ return total + player.score; }, 0) return ( <table className="stats"> <tbody> <tr> <td>Players:</td> <td>{totalPlayers}</td> </tr> <tr> <td>Total Points:</td> <td>{totalPoints}</td> </tr> </tbody> </table> ); } Stats.propTypes = { players: React.PropTypes.array.isRequired, }; function Header(props) { return ( <div className="header"> <Stats /> <h1>{props.title}</h1> <Stopwatch /> </div> ); } Header.propTypes = { title: React.PropTypes.string.isRequired, }; function Counter(props) { return ( <div className="counter"> <button className="counter-action decrement" onClick={function() {props.onChange(-1)}}> - </button> <div className="counter-score"> {props.score }</div> <button className="counter-action increment" onClick={function() {props.onChange(1)}}> + </button> </div> ); } Counter.propTypes = { score: React.PropTypes.number.isRequired, onChange: React.PropTypes.func.isRequired, } function Player(props) { return ( <div className="player"> <div className="player-name"> <a className="remove-player" onClick={props.onRemove}>x</a> {props.name} </div> <div className="player-score"> <Counter score={props.score} onChange={props.onScoreChange}/> </div> </div> ); } Player.propTypes = { name: React.PropTypes.string.isRequired, score: React.PropTypes.number.isRequired, onScoreChange: React.PropTypes.func.isRequired, onRemove: React.PropTypes.func.isRequired, } var Application = React.createClass({ propTypes: { title: React.PropTypes.string, initialPlayers: React.PropTypes.arrayOf(React.PropTypes.shape({ name: React.PropTypes.string.isRequired, score: React.PropTypes.number.isRequired, id: React.PropTypes.number.isRequired, })).isRequired, }, getDefaultProps: function() { return { title: "Scoreboard", } }, getInitialState: function() { return { players: this.props.initialPlayers, }; }, onScoreChange: function(index, delta) { // uncomment this to double check value change on the application // console.log('onScoreChange', index, delta); this.state.players[index].score += delta; this.setState(this.state); }, onPlayerRemove: function(index) { // uncomment to see the player index // console.log('remove', index); this.state.players.splice(index, 1); setState(this.state); }, onPlayerAdd: function() { // uncomment to see new player name //console.log('Player added:', name); this.state.players.push({ name: name, score: 0, id: nextId, }); /* NOTE: in something like Redux, we don't update the state itself, we actually create a brand new state object */ this.setState(this.state); nextId += 1; }; render: function() { return( <div className="scoreboard"> <Header title={this.props.title} players={this.state.players} /> <div className="players"> {this.state.players.map(function (player, index) { return ( <Player onScoreChange={function(delta) { this.onScoreChange(index,delta)}.bind(this) } onRemove={function() { this.onRemovePlayer(index); }.bind(this) } name={player.name} score{player.score} key={player.id} /> ); }.bind(this))} </div> <AddPlayerForm onAdd={this.onPlayerAdd} /> </div> ); } }); ReactDOM.render(<Application intialPlayers={PLAYERS}/>, document.getElementById('container'));
Stopwatch will either be running, or it won't be.
let PLAYERS = [ { name: "Dennis", score: 33, id:1, }, { name: "Ben", score: 34, id:2, }, { name: "Clark From InVision", score: 12, id:3, } ]; var nextId = 4; var StopWatch = React.createClass({ getInitialState: function () { return { running: false, }; }, onStop: function() { this.setState({ running: false }); }, onStart: function() { this.setState({ running: true }); }, onReset: function() { }, render: function() { return ( <div className="stopwatch"> <h2>Stopwatch</h2> <div className="stopwatch-time">0</div> { this.state.running ? <button onClick={this.onStop}>Stop</button> : <button onClick={this.onStart}>Start</button>; } <button onClick={this.onReset}>Reset</button> </div> ); } }); var AddPlayerForm = React.createClass({ propTypes: { onAdd: React.PropTypes.func.isRequired, }, getInitialState: function() { return { name: "" }; }, onNameChange: function(e) { // uncomment to see the changes on target value // console.log("onNameChange", e.target.value); this.setState({name: e.target.value}); }, onSubmit: function(e) { e.preventDefault(); this.props.onAdd(this.state.name); this.setState({name: ""}); }, render: function() { return ( <div className="add-player-form"> <form onSubmit={this.onSubmit}> <input type="text" value={this.state.name} onChange={this.onNameChange} /> <input type="submit" value="Add Player" /> </form> </div> ); } }); function Stats(props) { // you don't have to store it, but it's handy for organisation var totalPlayers = props.players.length; var totalPoints = props.players.reduce(function(total, player){ return total + player.score; }, 0) return ( <table className="stats"> <tbody> <tr> <td>Players:</td> <td>{totalPlayers}</td> </tr> <tr> <td>Total Points:</td> <td>{totalPoints}</td> </tr> </tbody> </table> ); } Stats.propTypes = { players: React.PropTypes.array.isRequired, }; function Header(props) { return ( <div className="header"> <Stats /> <h1>{props.title}</h1> <Stopwatch /> </div> ); } Header.propTypes = { title: React.PropTypes.string.isRequired, }; function Counter(props) { return ( <div className="counter"> <button className="counter-action decrement" onClick={function() {props.onChange(-1)}}> - </button> <div className="counter-score"> {props.score }</div> <button className="counter-action increment" onClick={function() {props.onChange(1)}}> + </button> </div> ); } Counter.propTypes = { score: React.PropTypes.number.isRequired, onChange: React.PropTypes.func.isRequired, } function Player(props) { return ( <div className="player"> <div className="player-name"> <a className="remove-player" onClick={props.onRemove}>x</a> {props.name} </div> <div className="player-score"> <Counter score={props.score} onChange={props.onScoreChange}/> </div> </div> ); } Player.propTypes = { name: React.PropTypes.string.isRequired, score: React.PropTypes.number.isRequired, onScoreChange: React.PropTypes.func.isRequired, onRemove: React.PropTypes.func.isRequired, } var Application = React.createClass({ propTypes: { title: React.PropTypes.string, initialPlayers: React.PropTypes.arrayOf(React.PropTypes.shape({ name: React.PropTypes.string.isRequired, score: React.PropTypes.number.isRequired, id: React.PropTypes.number.isRequired, })).isRequired, }, getDefaultProps: function() { return { title: "Scoreboard", } }, getInitialState: function() { return { players: this.props.initialPlayers, }; }, onScoreChange: function(index, delta) { // uncomment this to double check value change on the application // console.log('onScoreChange', index, delta); this.state.players[index].score += delta; this.setState(this.state); }, onPlayerRemove: function(index) { // uncomment to see the player index // console.log('remove', index); this.state.players.splice(index, 1); setState(this.state); }, onPlayerAdd: function() { // uncomment to see new player name //console.log('Player added:', name); this.state.players.push({ name: name, score: 0, id: nextId, }); /* NOTE: in something like Redux, we don't update the state itself, we actually create a brand new state object */ this.setState(this.state); nextId += 1; }; render: function() { return( <div className="scoreboard"> <Header title={this.props.title} players={this.state.players} /> <div className="players"> {this.state.players.map(function (player, index) { return ( <Player onScoreChange={function(delta) { this.onScoreChange(index,delta)}.bind(this) } onRemove={function() { this.onRemovePlayer(index); }.bind(this) } name={player.name} score{player.score} key={player.id} /> ); }.bind(this))} </div> <AddPlayerForm onAdd={this.onPlayerAdd} /> </div> ); } }); ReactDOM.render(<Application intialPlayers={PLAYERS}/>, document.getElementById('container'));
let PLAYERS = [ { name: "Dennis", score: 33, id:1, }, { name: "Ben", score: 34, id:2, }, { name: "Clark From InVision", score: 12, id:3, } ]; var nextId = 4; var StopWatch = React.createClass({ getInitialState: function () { return { running: false, elapsedTime: 0, previousTime: 0, }; }, componentDidMount: function() { this.interval = setInterval(this.onTick, 100); }, componentWillUnmount: function() { clearInterval(this.setInterval); }, onTick: function() { //uncomment to confirm the tick in the console. //console.log('onTick'); if (this.state.running) { var now = Date.now(); this.setState({ previousTime: now, elapsedTime: this.state.elapsedTime + (now - this.state.previousTime), }); } }, onStop: function() { this.setState({ running: false }); }, onStart: function() { this.setState({ running: true, previousTime: Date.now(), }); }, onReset: function() { this.setState({ elapsedTime: 0, previousTime: Date.now(), }); }, render: function() { var seconds = Math.floor(this.state.elapsedTime / 1000); return ( <div className="stopwatch"> <h2>Stopwatch</h2> <div className="stopwatch-time">{seconds}</div> { this.state.running ? <button onClick={this.onStop}>Stop</button> : <button onClick={this.onStart}>Start</button>; } <button onClick={this.onReset}>Reset</button> </div> ); } }); var AddPlayerForm = React.createClass({ propTypes: { onAdd: React.PropTypes.func.isRequired, }, getInitialState: function() { return { name: "" }; }, onNameChange: function(e) { // uncomment to see the changes on target value // console.log("onNameChange", e.target.value); this.setState({name: e.target.value}); }, onSubmit: function(e) { e.preventDefault(); this.props.onAdd(this.state.name); this.setState({name: ""}); }, render: function() { return ( <div className="add-player-form"> <form onSubmit={this.onSubmit}> <input type="text" value={this.state.name} onChange={this.onNameChange} /> <input type="submit" value="Add Player" /> </form> </div> ); } }); function Stats(props) { // you don't have to store it, but it's handy for organisation var totalPlayers = props.players.length; var totalPoints = props.players.reduce(function(total, player){ return total + player.score; }, 0) return ( <table className="stats"> <tbody> <tr> <td>Players:</td> <td>{totalPlayers}</td> </tr> <tr> <td>Total Points:</td> <td>{totalPoints}</td> </tr> </tbody> </table> ); } Stats.propTypes = { players: React.PropTypes.array.isRequired, }; function Header(props) { return ( <div className="header"> <Stats /> <h1>{props.title}</h1> <Stopwatch /> </div> ); } Header.propTypes = { title: React.PropTypes.string.isRequired, }; function Counter(props) { return ( <div className="counter"> <button className="counter-action decrement" onClick={function() {props.onChange(-1)}}> - </button> <div className="counter-score"> {props.score }</div> <button className="counter-action increment" onClick={function() {props.onChange(1)}}> + </button> </div> ); } Counter.propTypes = { score: React.PropTypes.number.isRequired, onChange: React.PropTypes.func.isRequired, } function Player(props) { return ( <div className="player"> <div className="player-name"> <a className="remove-player" onClick={props.onRemove}>x</a> {props.name} </div> <div className="player-score"> <Counter score={props.score} onChange={props.onScoreChange}/> </div> </div> ); } Player.propTypes = { name: React.PropTypes.string.isRequired, score: React.PropTypes.number.isRequired, onScoreChange: React.PropTypes.func.isRequired, onRemove: React.PropTypes.func.isRequired, } var Application = React.createClass({ propTypes: { title: React.PropTypes.string, initialPlayers: React.PropTypes.arrayOf(React.PropTypes.shape({ name: React.PropTypes.string.isRequired, score: React.PropTypes.number.isRequired, id: React.PropTypes.number.isRequired, })).isRequired, }, getDefaultProps: function() { return { title: "Scoreboard", } }, getInitialState: function() { return { players: this.props.initialPlayers, }; }, onScoreChange: function(index, delta) { // uncomment this to double check value change on the application // console.log('onScoreChange', index, delta); this.state.players[index].score += delta; this.setState(this.state); }, onPlayerRemove: function(index) { // uncomment to see the player index // console.log('remove', index); this.state.players.splice(index, 1); setState(this.state); }, onPlayerAdd: function() { // uncomment to see new player name //console.log('Player added:', name); this.state.players.push({ name: name, score: 0, id: nextId, }); /* NOTE: in something like Redux, we don't update the state itself, we actually create a brand new state object */ this.setState(this.state); nextId += 1; }; render: function() { return( <div className="scoreboard"> <Header title={this.props.title} players={this.state.players} /> <div className="players"> {this.state.players.map(function (player, index) { return ( <Player onScoreChange={function(delta) { this.onScoreChange(index,delta)}.bind(this) } onRemove={function() { this.onRemovePlayer(index); }.bind(this) } name={player.name} score{player.score} key={player.id} /> ); }.bind(this))} </div> <AddPlayerForm onAdd={this.onPlayerAdd} /> </div> ); } }); ReactDOM.render(<Application intialPlayers={PLAYERS}/>, document.getElementById('container'));
What did we learn?