Build a user interface with React
ECA React - December 5th 2018
Last time...
-
Render classes dynamically
-
Render lists
-
Conditional rendering
-
Handling events
-
Composing components
-
Passing data to components
-
State vs props
State
- Local to the component
- Other components cannot access it
Props
- Passed to a component
- Once passed to the component, it cannot be modified
VS
Let's continue !
+
-
3
+
-
+
-
+
-
1
+
-
Total
10
Reset
Delete
Delete
Delete
Delete
Delete
Zero
Zero
6
Zero
Zero
Increment
5
2
3
Increment
Increment
Increment
Increment
Passing data to components
-
Data can be passed through attributes
-
<PokeCounter key={counter.id} count={counter.count} anotherAttribute="a value I pass to my component"/>
-
You can then retrieve the data from a plain JavaScript object called props
-
key will not be passed as it is a special keyword used to identify elements in a list
pokeCounter.jsx
import React, { Component } from "react";
import PokeCounter from "./pokeCounter";
class PokeCounters extends Component {
state = {
counters: [
{ id: 1, value: 0 }
{ id: 2, value: 0 },
{ id: 3, value: 0 },
{ id: 4, value: 0 },
{ id: 5, value: 0 }
]
};
render() {
return (
<div>
{this.state.counters.map(counter =>
<PokeCounter
key={counter.id}
value={counter.value}
/>
)}
</div>
);
}
}
export default PokeCounters;
import React, { Component } from "react";
class PokeCounter extends Component {
state = {
value: this.props.value
};
/** [...] */
incrementCount = () => {
this.setState({
value: this.state.value + 1
});
};
render() {
return (
<React.Fragment>
<span
className={this.getBadgesClasses()}>
{this.formatCount()}
</span>
<button
onClick={this.incrementCount}
className="btn btn-secondary btn-sm"
>
Increment
</button>
</React.Fragment>
);
}
}
export default PokeCounter;
pokeCounters.jsx
-
In case of a complex element, you can pass data as children props
-
<PokeCounter key={counter.id} count={counter.count}> <h1>Hello world</h1> </PokeCounter>
-
data is then accessible in props.children
React dev tools
Available in Chrome and Firefox
Raising and handling events
2
Increment
3
5
0
0
Delete
Increment
Delete
Increment
Delete
Increment
Delete
Increment
Delete
Let's add "Delete" button
PokeCounters
PokeCounter
state = {
counters: []
};
Delete button
How to update that state?
The component that owns a piece of state should be the one modifying it
PokeCounters
PokeCounter
Raise the event: delete
Handle the event: delete
PokeCounters
PokeCounter
this.props.onDelete
handleDelete
<PokeCounter onDelete={this.handleDelete}>
class PokeCounters extends Component {
/** [...] */
handleDelete = () => {
console.log("Event handler called");
};
render() {
return (
<div>
{this.state.counters.map(counter => (
<PokeCounter
key={counter.id}
value={counter.value}
onDelete={this.handleDelete}
/>
))}
</div>
);
}
}
class PokeCounter extends Component {
/** [...] */
render() {
return (
<div>
/** [...] */
<button
className="btn btn-danger btn-sm-m-2"
onClick={this.props.onDelete}
>
Delete
</button>
</div>
);
}
}
Update the state
class PokeCounters extends Component {
/** [...] */
handleDelete = counterId => {
const counters = this.state.counters.filter(c => c.id !== counterId);
this.setState({ counters });
};
render() {
return (
<div>
{this.state.counters.map(counter => (
<PokeCounter
key={counter.id}
value={counter.value}
id={counter.id}
onDelete={this.handleDelete}
/>
))}
</div>
);
}
}
class PokeCounter extends Component {
/** [...] */
render() {
return (
<div>
/** [...] */
<button
className="btn btn-danger btn-sm-m-2"
onClick={() => this.props.onDelete(this.props.id)}
>
Delete
</button>
</div>
);
}
}
class PokeCounters extends Component {
/** ... */
render() {
return (
<div>
{this.state.counters.map(counter => (
<PokeCounter
key={counter.id}
counter={counter}
onDelete={this.handleDelete}
/>
))}
</div>
);
}
}
class PokeCounter extends Component {
state = {
value: this.props.counter.value
};
/** ... */
render() {
return (
<div>
/** ... */
<button
className="btn btn-danger btn-sm-m-2"
onClick={() => this.props.onDelete(this.props.counter.id)}
>
Delete
</button>
</div>
);
}
}
Single Source of Truth
2
Increment
3
5
0
0
Delete
Increment
Delete
Increment
Delete
Increment
Delete
Increment
Delete
Reset
Let's add "Reset" button
class PokeCounters extends Component {
/** [...] */
handleReset = () => {
const counters = this.state.counters.map(c => {
c.value = 0;
return c;
});
this.setState({ counters });
};
render() {
return (
<div>
<button
className="btn btn-primary btn-sm-m-2"
onClick={this.handleReset}
>
Reset
</button>
/** [...] */
</div>
</div>
);
}
}
It does not work because PokeCounter has a local state
Removing the local state
PokeCounters
PokeCounter
Controlled component
data (props)
raise events
import React, { Component } from "react";
class PokeCounter extends Component {
formatCount() {
return this.props.counter.value === 0 ? "Zero" : this.props.counter.value;
}
getBadgesClasses() {
let classes = "badge m-2 badge-";
classes += this.props.counter.value === 0 ? "warning" : "primary";
return classes;
}
render() {
return (
<div>
<span className={this.getBadgesClasses()}>{this.formatCount()}</span>
<button
onClick={() => this.props.onIncrement(this.props.counter)}
className="btn btn-secondary btn-sm m-2"
>
Increment
</button>
<button
className="btn btn-danger btn-sm-m-2"
onClick={() => this.props.onDelete(this.props.counter.id)}
>
Delete
</button>
</div>
);
}
}
export default PokeCounter;
import React, { Component } from "react";
import PokeCounter from "./pokeCounter";
class PokeCounters extends Component {
state = {
counters: [
{ id: 1, value: 1 },
{ id: 2, value: 0 },
{ id: 3, value: 0 },
{ id: 4, value: 0 },
{ id: 5, value: 0 }
]
};
handleDelete = counterId => {
const counters = this.state.counters.filter(c => c.id !== counterId);
this.setState({ counters });
};
handleIncrement = counter => {
const counters = [...this.state.counters];
const index = counters.indexOf(counter);
counters[index] = { ...counters[index] };
counters[index].value++;
this.setState({ counters });
};
handleReset = () => {
const counters = this.state.counters.map(c => {
c.value = 0;
return c;
});
this.setState({ counters });
};
render() {
return (
<div>
<button
className="btn btn-primary btn-sm-m-2"
onClick={this.handleReset}
>
Reset
</button>
<div>
{this.state.counters.map(counter => (
<PokeCounter
key={counter.id}
counter={counter}
onDelete={this.handleDelete}
onIncrement={this.handleIncrement}
/>
))}
</div>
</div>
);
}
}
export default PokeCounters;
Multiple components in sync
App
TotalBar
PokeCounters
PokeCounter
counters[]
import React, { Component, Fragment } from "react"
import TotalBar from "./components/totalBar"
import PokeCounters from "./components/pokeCounters"
class App extends Component {
state = {
counters: [
{ id: 1, value: 1 },
{ id: 2, value: 0 },
{ id: 3, value: 0 },
{ id: 4, value: 0 },
{ id: 5, value: 0 }
]
}
handleDelete = counterId => {
const counters = this.state.counters.filter(c => c.id !== counterId)
this.setState({ counters })
}
handleIncrement = counter => {
const counters = [...this.state.counters]
const index = counters.indexOf(counter)
counters[index] = { ...counters[index] }
counters[index].value++
this.setState({ counters })
}
handleReset = () => {
const counters = this.state.counters.map(c => {
c.value = 0
return c
})
this.setState({ counters })
}
render() {
return (
<Fragment>
<TotalBar
totalCount={this.state.counters.reduce(
(acc, curr) => acc + curr.value,
0
)}
/>
<main className="container">
<PokeCounters
counters={this.state.counters}
onReset={this.handleReset}
onIncrement={this.handleIncrement}
onDelete={this.handleDelete}
/>
</main>
</Fragment>
)
}
}
export default App
import React, { Component } from "react"
import PokeCounter from "./pokeCounter"
class PokeCounters extends Component {
render() {
const { counters, onReset, onIncrement, onDelete } = this.props
return (
<div>
<button className="btn btn-primary btn-sm-m-2" onClick={onReset}>
Reset
</button>
<div>
{counters.map(counter => (
<PokeCounter
key={counter.id}
counter={counter}
onDelete={onDelete}
onIncrement={onIncrement}
/>
))}
</div>
</div>
)
}
}
export default PokeCounters
import React, { Component } from "react"
class PokeCounter extends Component {
formatCount() {
return this.props.counter.value === 0 ? "Zero" : this.props.counter.value
}
getBadgesClasses() {
let classes = "badge m-2 badge-"
classes += this.props.counter.value === 0 ? "warning" : "primary"
return classes
}
render() {
return (
<div>
<span className={this.getBadgesClasses()}>{this.formatCount()}</span>
<button
onClick={() => this.props.onIncrement(this.props.counter)}
className="btn btn-secondary btn-sm m-2">
Increment
</button>
<button
className="btn btn-danger btn-sm-m-2"
onClick={() => this.props.onDelete(this.props.counter.id)}>
Delete
</button>
</div>
)
}
}
export default PokeCounter
import React from "react"
const TotalBar = ({ totalCount }) => {
return (
<nav className="navbar navbar-light bg-light">
<div>
Total: <span className="badge m-2 badge-info">{totalCount}</span>
</div>
</nav>
)
}
export default TotalBar
Stateless functional components
Exercice
3
Zero
6
1
Total
10
Reset
Delete
Delete
+
-
3
+
-
+
-
+
-
1
+
-
Total
10
Reset
Delete
Delete
Delete
Delete
Delete
Increment
Increment
Zero
Delete
Increment
Zero
Zero
6
Delete
Increment
Delete
Increment
Exercice
- Add Pokeball images
- Align in columns
-
Create "-" (decrement) button
- It should decrement corresponding value
- It should be disabled when corresponding value is Zero
Correction
See you next wednesday!
React (3/5) - Raise and handle events
By zolani
React (3/5) - Raise and handle events
ECA React - December 5th 2018
- 741