TIL/2021–02–01&02
--
Day 53 and 54: Treehouse Full Stack JavaScript Techdegree
Unidirectional Data Flow
In React data naturally flows down the component tree, from the app’s top level component down to the child components via props
For example, in our scoreboard, the App component tells the player component all about a player by passing information via props, it also tells the header component the total number of players
If our data comes from one place, React will flow any data changes from the top going down to the component tree, updating each component
And this is called unidirectional data flow
— — —
Lifting State Up
In React, two or more components can share the same state. For example, in our App component, the players state is shared to the Header and Player components through props.
Our Counter component has the score state which can only be accessed locally within the counter component since that is where we set the state. Eventually, we need access to the score state to determine who has the highest score and such
When two or more components needs access to the same state, we move the state into the common parent which is called lifting state up.
So we will lift up the score state to the app component which is the common parent
— —
Communicating between components
The buttons in the counter need to modify the state that lives in the app component
When the data flow is unidirectional (data changes goes from top to bottom) how does the child component like counter gets information back up to its ancestor parent component? Instead of passing state to a child component, a parent can pass a callback function
The callback will allow you to communicate events and changes in your data upwards, while data continues to flow downwards
We can write event handlers that manipulate state and pass them down to components as callback functions
On app.js
import React, { Component } from “react”;
import Header from “./Header.js”;
import Player from “./Player.js”;
class App extends Component {
state = {
players: [
{
name: “Guil”,
score: 0,
id: 1,
},
{
name: “Treasure”,
score: 0,
id: 2,
},
{
name: “Ashley”,
score: 0,
id: 3,
},
{
name: “James”,
score: 0,
id: 4,
},
],
};
handleRemovePlayer = (id) => {
this.setState((prevState) => {
return {
players: prevState.players.filter((p) => p.id !== id),
};
});
};
handleScoreChange = (delta) => {
// this.setState((prevState) => ({
// score: prevState.score + delta,
// }));
console.log(delta);
};
render() {
return (
<div className=”scoreboard”>
<Header title=”Scoreboard” totalPlayers={this.state.players.length} />
{/* Players list */}
{this.state.players.map((player) => (
<Player
{…player}
handleScoreChange={this.handleScoreChange}
key={player.id.toString()}
removePlayer={this.handleRemovePlayer}
/>
))}
</div>
);
}
}
export default App;
Then on Player.js add the handleScoreChange as a prop to pass on to counter
Counter
score={props.score}
handleScoreChange={props.handleScoreChange}
/>
Then on Counter
We can add an onClick event handler with a callback function to handleScoreChange
import React from “react”;
const Counter = (props) => {
return (
<div className=”counter”>
<button
className=”counter-action decrement”
onClick={() => {
props.handleScoreChange(-1);
}}
>
{“ “}
-{“ “}
</button>
<span className=”counter-score”>{props.score}</span>
<button
className=”counter-action increment”
onClick={() => props.handleScoreChange(+1)}
>
{“ “}
+{“ “}
</button>
</div>
);
};
export default Counter;
Don’t forget, it has to be an arrow function to be a callback
— —
Update state based on a player’s index
The map callback function takes an optional index parameter that contains the index of the current item being processed in the array
handleScoreChange = (delta, index) => {
// this.setState((prevState) => ({
// score: prevState.score + delta,
// }));
console.log(`index:${index}, delta:${delta}`);
};
{this.state.players.map((player, index) => (
<Player
index={index}
{…player}
handleScoreChange={this.handleScoreChange}
key={player.id.toString()}
removePlayer={this.handleRemovePlayer}
/>
))}
On Player
<Counter
index={props.index}
score={props.score}
handleScoreChange={props.handleScoreChange}
/>
On Counter
const Counter = (props) => {
const index = props.index;
return (
<div className=”counter”>
<button
className=”counter-action decrement”
onClick={() => {
props.handleScoreChange(-1, index);
}}
>
{“ “}
-{“ “}
</button>
<span className=”counter-score”>{props.score}</span>
<button
className=”counter-action increment”
onClick={() => props.handleScoreChange(+1, index)}
>
{“ “}
+{“ “}
</button>
</div>
);
};
Now each player logs their distinct index
handleScoreChange = (delta, index) => {
this.setState((prevState) => ({
score: (prevState.players[index].score += delta),
}));
};
And now it works
— — -
Building the statistics component
The problem with out old structure is that the score state was kept locally in the Counter component — and if we need access to the total scores we cannot do that. But since we were able to lift up the state to the common or main parent, any component can have access to the score state.
On app.js
<Header title=”Scoreboard” players={this.state.players} />
On header.js
import React from “react”;
import Stats from “./Stats”;
const Header = (props) => {
return (
<header>
<Stats players={props.players} />
<h1>{props.title}</h1>
</header>
);
};
export default Header;
Then the newly created Stat.js
import React from “react”;
const Stats = (props) => {
const totalPlayers = props.players.length;
const totalPoints = props.players.reduce((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>
);
};
export default Stats;
— — —
Controlled components
Adding players
For JSX, JSX requires input tags to be a self-closing element
In React, we need to handle a form element’s state explicitly.
Normally, when a user types into an input text field the user changes the state of the text field
To manage our input field’s state we have to build a controlled component — a controlled component renders a form that controls what happens in that form on subsequent user input.
It is a form element whose value is controlled by React with a state.
Creating a controlled component:
- Initialize state for the value of the input
- Listen for changes on the input to detect when the value is updated
- Create an event handler that updates the value state
import React, { Component } from “react”;
class AddPlayerForm extends Component {
state = {
value: “”,
};
handleValueChange = (e) => {
this.setState({ value: e.target.value });
};
render() {
console.log(this.state.value);
return (
<form>
<input
type=”text”
value={this.state.value}
onChange={this.handleValueChange}
placeholder=”Enter a player’s name”
/>
<input type=”submit” value=”Add Player” />
</form>
);
}
}
export default AddPlayerForm;
The state updates as we type
We now have a controlled input, meaning that we manually managed the value state of the component
— — —
Adding Items to State
Currently the AddPlayerForm component has no access to the players state in App component and the AddPlayerForm state is a local state
In order to add a new player to the state, AddPlayerForm needs access to the players state. So that it can update it with the submitted data
If we don’t do .preventDefault() — it will result in the browser posting a request back to the server. That would cause our application to reload in the browser — meaning we would lose the data we just typed
addPlayerForm.js
import React, { Component } from “react”;
class AddPlayerForm extends Component {
state = {
value: “”,
};
handleValueChange = (e) => {
this.setState({ value: e.target.value });
};
handleSubmit = (e) => {
e.preventDefault();
this.props.addPlayer(this.state.value);
this.state.value = “”;
//or use this.setState({value: ‘’})
};
render() {
return (
<form onSubmit={this.handleSubmit}>
<input
type=”text”
value={this.state.value}
onChange={this.handleValueChange}
placeholder=”Enter a player’s name”
/>
<input type=”submit” value=”Add Player” />
</form>
);
}
}
export default AddPlayerForm;
App.js
handleAddPlayer = (name) => {
this.setState({
players: [
…this.state.players, //Represents all existing players on the scoreboard. We are just adding the following object
{
name: name,
score: 0,
id: this.state.players.length + 1,
},
],
});
};
<AddPlayerForm addPlayer={this.handleAddPlayer} />
We used the spread operator to unpack the array and then add the new player
— — — -
Update the players state based on the previous state
handleAddPlayer = (name) => {
this.setState((prevState) => {
return {
players: [
…prevState.players,
{
name: name,
score: 0,
id: this.state.players.length + 1,
},
],
};
});
};
handleAddPlayer = (name) => {
const newPlayer = {
name,
score: 0,
id: this.state.players.length + 1,
};
this.setState((prevState) => {
return {
players: prevState.players.concat(newPlayer),
};
});
};
If not return
handleAddPlayer = (name) => {
const newPlayer = {
name,
score: 0,
id: this.state.players.length + 1,
};
this.setState((prevState) => ({
players: prevState.players.concat(newPlayer),
}));
};
By just using the () parenthesis in replacement of the curly brackets {}
— — -
React flows any data changes at the top down through the component tree updating each component
Can two or more components access and share the same state? Yes. A parent component can pass state down to its children via props
When two or more components need access to the same state, we hoist the state into their common parent — lifting state