- Download or clone
git clone https://github.com/janakanuwan/CS3249-2020-React-Exercise.git
- Go to the React root directory/folder (e.g.
part-1
orpart-2
)
cd part-1
- Install dependencies (will create
node_modules
directory)
npm install
- Start the application
npm start
We want to change the UI as follows:
Data Source: SuperHero API
- What is the view tree?
- What are the view components?
- What are the properties of view components?
- Create view components using React.
EXERCISE: Add the power list
- What is
props
?- How the variables are accessed in JSX? [NOTE: curly brackets]
// src/view/PowerStatsView.jsx
import React from 'react';
class PowerStatsView extends React.Component{
// NOTE: render method should be implemented if you use ES6 classes
render(){
// NOTE: we extract the data first from 'props'
const {data} = this.props;
// NOTE: In list we have to add 'key' attribute. Why?
const powerList = Object.entries(data).map((name, index) =>
<li key={index}> {name[0]} : {name[1]} </li>
);
return (
<div>
<p>Powers: </p>
{/* NOTE: JSX comments */}
{/* EXERCISE: add the power list */}
</div>
);
}
}
// NOTE: you need to 'export' the component to access it by other files (if all components are in a single file, you do not need to 'export')
export default PowerStatsView;
- What is meant by 'export'? Hint: Modules
/* src/view/hero-view.css */
.hero-view {
width: 170px;
margin: 10px;
border: 1px solid #000;
border-radius: 5px;
padding: 20px;
display: flex;
flex-direction: column;
justify-content: flex-start;
}
.hero-view > img {
width: 150px;
height: 150px;
margin: 5px;
}
EXERCISE: Import the PowerStatsView and add it to HeroView
// src/view/HeroView.jsx
import React from 'react'
// importing React Component
// EXERCISE: import the PowerStatsView and add it
// NOTE: Do we need to add ".jsx" extension?
// import css
import './hero-view.css'
// NOTE: React component starts with Capital letter
class HeroView extends React.Component{
render(){
// NOTE: we extract the data first from 'props'
const {hero} = this.props;
return (
// EXERCISE: pass css property 'class-name' = "hero-view"
<div >
<h3>{hero.name}</h3>
<img src={hero.image} alt={hero.image}/>
{/* NOTE: we can use the child component directly in JSX */}
{/* NOTE: we pass the data by a known name (i.e. 'data') */}
{/*EXERCISE: Add the PowerStatsView here and pass the 'powerstats' from 'hero' */}
</div>
);
}
}
export default HeroView;
/* src/view/publisher-panel.css */
.publisher-panel {
display: flex;
flex-flow: row wrap;
}
.publisher-panel-header {
flex:1 0 100%;
}
// src/model/HeroView.js
import React from 'react';
import HeroView from './HeroView'
import './publisher-view.css'
class PublisherView extends React.Component{
constructor(props) {
super(props);
}
render(){
const {heroes} = this.props;
// What will happen if heroes array is empty?
const publisher = heroes[0]["publisher"];
// NOTE: HTML uses simple names for its attributes, but JSX uses camel case!
return (
<div>
<h2>Super Heroes from {publisher} </h2>
<div className={"publisher-view"}>
{/* Use JS map function to create multiple heroes with looping*/}
{heroes.map( (heroData) =>
<HeroView key={heroData.id} hero={heroData}/>)}
</div>
</div>
);
}
}
export default PublisherView;
- Create the data classes using JS.
- NOTE: For the assigment 3, you may not need to separate them!
// src/model/PowerStats.model.js
class PowerStats {
constructor(intelligence, strength, speed) {
// constructor function to initialize object
this.intelligence = intelligence;
this.strength = strength;
this.speed = speed;
}
}
export default PowerStats;
// src/model/Hero.model.js
class Hero{
constructor(id, name, powerstats = {}, image = '', publisher = ''){
this.id = id;
this.name = name
this.powerstats = powerstats;
this.image = image;
this.publisher = publisher;
}
}
export default Hero;
- Render the react components from
src/App.js
- We will use data from a file to initialize the page
import React from 'react';
import './App.css';
// React component
import PublisherView from './view/PublisherView'
// model classes
import Hero from './model/Hero.model'
import PowerStats from "./model/PowerStats.model";
// data [NOTE: generally data comes from a database or a backend server]
import rawData from './data'
// convert the data to supported format
const parseHeroData =(dataArray = []) => dataArray.map( (dataItem) =>
new Hero(
dataItem.id,
dataItem.name,
new PowerStats(dataItem.powerstats.intelligence, dataItem.powerstats.strength, dataItem.powerstats.speed),
dataItem.image,
dataItem.publisher
)
);
// filter by publisher
const filteredHeroesByPublisher = (publisher, heroDataArray) =>
heroDataArray.filter(hero => publisher === hero.publisher);
// App Component in functional format instead of extending from React.Component
function App() {
// initialize
const data = parseHeroData(rawData);
const publishers = ['Marvel Comics', 'DC Comics'];
return (
<div>
{/* use the JS functions directly inside the JSX syntax! */}
{publishers.map( (publisher) => <PublisherView key={publisher} heroes={filteredHeroesByPublisher(publisher, data)} />)}
</div>
);
}
export default App;
We want to change the UI as follows [NOTE: Now 'Powers' are shown in a bar chart!]:
- Go to
part-2
directory and start the app
npm install
npm start
npm i -S recharts
# or 'npm install --save recharts'
- Why is it added as (production) dependency?
- Add a barchart to
src/view/PowerStatsChart.jsx
- Note how the BarChart component was imported and used!
// src/view/PowerStatsChart.jsx
import React from 'react'
import {
BarChart, Bar, Cell, XAxis, YAxis, CartesianGrid, Tooltip, Legend,
} from 'recharts'
class PowerStatsChart extends React.Component{
render(){
// NOTE: data format should be directly supported
const {data} = this.props;
return (
<BarChart
width={200}
height={200}
data={data}
margin={{
top: 5, right: 5, left: 0, bottom: 5,
}}
>
<CartesianGrid strokeDasharray="3 3"/>
<XAxis dataKey="name"/>
<YAxis/>
<Tooltip/>
<Legend/>
{/* What is meany by 'dataKey'? */}
<Bar dataKey="intelligence" fill="#8884d8"/>
<Bar dataKey="strength" fill="#82ca9d"/>
<Bar dataKey="speed" fill="#ffc658"/>
</BarChart>
);
}
}
export default PowerStatsChart;
- Modify the
PowerStatsView.jsx
as follows.
EXERCISE: Add the 'PowerStatsChart'
// src/view/PowerStatsChart.jsx
import React from 'react'
import PowerStatsChart from "./PowerStatsChart";
class PowerStatsView extends React.Component {
// static utility function that maps the data parsed from props to Chart supported format
static graphDataMapper(data){
const {intelligence, strength, speed} = data;
return (
[
{
name: 'Powers', intelligence: intelligence, strength: strength, speed: speed,
}
]
);
}
render() {
const {data} = this.props;
return (
<div>
{/* EXERCISE: Add the 'PowerStatsChart'*/}
</div>
);
}
}
export default PowerStatsView;
- What is the relationship between
<Bar dataKey='power1' />
and graph legends and values?- What is the relationship between
<XAxis dataKey='name' />
and graph X-label?- How to fix the legends to show 'intelligence', 'strength' and 'speed'?
- Let's add buttons to
src/view/HeroView.jsx
// src/view/HeroView.jsx
class HeroView extends React.Component{
render(){
const {hero} = this.props;
return (
<div className={"hero-view"}>
<h3>{hero.name}</h3>
<img src={hero.image} alt={hero.image}/>
{/* New buttons */}
<button>Increase Strength</button>
<button>Decrease Strength</button>
<PowerStatsView data={hero.powerstats}/>
</div>
);
}
}
- Let's handle events to change the chart values
EXERCISE: Add 'increaseStrength()' and 'increaseStrength()' functions
// src/view/HeroView.jsx
class HeroView extends React.Component{
constructor(props){
super(props);
const {hero} = props;
// This binding is necessary to make `this` work in the callback
this.increaseStrength = this.increaseStrength.bind(this);
this.decreaseStrength = this.decreaseStrength.bind(this);
}
// functions to handle clicks
increaseStrength(e) {
// TODO
console.log(e);
}
decreaseStrength(e) {
// TODO
console.log(e);
}
render(){
const {hero} = this.props;
return (
<div className={"hero-view"}>
<h3>{hero.name}</h3>
<img src={hero.image} alt={hero.image}/>
<button onClick={this.increaseStrength}>Increase Strength</button>
<button onClick={this.decreaseStrength}>Decrease Strength</button>
<PowerStatsView data={hero.powerstats}/>
</div>
);
}
}
- Why 'onClick' is in camel case instead of 'onclick' as in HTML button format?
- Why do you have to bind functions inside the constructor?
onClick={this.increaseStrength}
does not have brackets like,this.increaseStrength()
. Why?
- Using local state to show values
- Where should we keep that state? How do we decide that? Hint: Thinking in React
- Which data should we keep in the state?
- What are the difference between state abd props?
*EXERCISE: Add state using this.state inside constructor function
// src/view/HeroView.jsx
class HeroView extends React.Component{
constructor(props){
super(props);
const {hero} = props;
// NOTE: adding local state to class
// NOTE: we create a local state, and set the initial values from 'props'
this.state = {
intelligence: hero.powerstats.intelligence,
strength: hero.powerstats.strength,
speed: hero.powerstats.speed
};
// This binding is necessary to make `this` work in the callback
this.increaseStrength = this.increaseStrength.bind(this);
this.decreaseStrength = this.decreaseStrength.bind(this);
}
// functions
render(){
const {hero} = this.props;
return (
<div className={"hero-view"}>
<h3>{hero.name}</h3>
<img src={hero.image} alt={hero.image}/>
<button onClick={this.increaseStrength}>Increase Strength</button>
<button onClick={this.decreaseStrength}>Decrease Strength</button>
{/* NOTE: Now we use 'state' instead of 'props' */}
<PowerStatsView data={this.state}/>
</div>
);
}
}
- Changing the state based on events
- What is the expected behavior of the state when you click "Increase Strength" (or "Decrease Strength")?
- How can we change the state? Hint: Use the inbuilt
this.setState()
to update the component's local state - IMPORTANT: Three things you should know about setState() - Do Not Modify State Directly - State Updates May Be Asynchronous - State Updates are Merged
// src/view/HeroView.jsx
// ...
increaseStrength() {
// NOTE: we do not directly change the state since it should be immutable
// NOTE: we can assign a new value to a property of the state instead of replacing whole state!
console.log("Before clicking Increase Strength Button");
console.log(this.state);
this.setState( state => ({
strength: state.strength + 5
}));
console.log("After clicking Increase Strength Button");
console.log(this.state);
}
decreaseStrength() {
this.setState( state => ({
strength: state.strength - 5
}));
}
// ...
- [Optional] How do we move the
state
to support MV* patterns?- Pass the functions with
props
- See Lifting State Up
- Pass the functions with