Building an app according to "thinking in react"
Pavlos Papadopoulos
Posted on June 2, 2020
If we dig in React Docs we will find a great article called “thinking in react”. It’s the last chapter of the main concepts, so it's essential to go through all the previous chapters before continuing here. In this article we'll build a simple app with react, by following the 5 steps in the chapter.
(the code for this app lives on github)
First things first: we draw a mockup either on paper or using software - there are many out there.
Step 1: Break The UI Into A Component Hierarchy
We have five components in our app.
- App (green): it's the top level component, contains everything inside the app
- AddDate (red): receives the date input from user
- DateList (brown): displays a list of cards based on the date user input
- Date (blue): displays a card for each date and receives the task input from user
- Task (orange): displays the task paragraph
Our components hierarchy is:
- App
- AddDate
- DateList
- Date
- Task
- Date
(components that appear within another component in the mock should appear as a child in the hierarchy)
Step 2: Build A Static Version in React
Now it's time to add the components so that we get a static layout of our app. In this step no interactivity is involved. As the docs mention, it’s usually easier in simple apps to build our components top-down (in our example starting from App component).
App
import React, { Component } from 'react';
import './App.css';
import AddDate from './AddDate';
import DateList from './DateList';
class App extends Component {
render() {
const dates = ['2018-04-23', '2019-06-13', '2014-09-29'];
return (
<div className="App">
<header className="App-header">
<h1>Time Machine</h1>
</header>
<AddDate dates={dates} />
<DateList dates={dates} />
</div>
);
}
}
export default App;
AddDate
import React, { Component } from 'react';
class AddDate extends Component {
render() {
return (
<div className="App__form">
<form className="App__form--date">
<div className="App__form--body">
<label>Choose Your Past:</label>
<input type="date" max={new Date().toISOString().split('T')[0]} />
</div>
<div className="App__form--btn">
<button type="submit">Add Date</button>
</div>
</form>
</div>
);
}
}
export default AddDate;
DateList
import React, { Component } from 'react';
import Date from './Date';
class DateList extends Component {
render() {
const { dates } = this.props;
return (
<div className="App__list">
<h2 className="App__list--title">Missions</h2>
<ul className="App__list--items">
{dates.map((date) => (
<Date date={date} key={date} />
))}
</ul>
</div>
);
}
}
export default DateList;
Date
import React, { Component } from 'react';
import Task from './Task';
class Date extends Component {
render() {
const { date } = this.props;
return (
<li>
<div className="App__card--inner">
<h2>{date}</h2>
<form onSubmit={this.handleFormSubmit} className="App__card">
<div className="App__card--form">
<label>Add Your Task</label>
<textarea
rows="3"
cols="30"
placeholder="type here..."
required
></textarea>
</div>
<div className="App__card--btn">
<button type="submit">Add Task</button>
</div>
</form>
<Task />
</div>
</li>
);
}
}
export default Date;
Task
import React from 'react';
const Task = () => {
return (
<div className="App__task">
<h3>Task</h3>
<p>this is the task paragraph</p>
</div>
);
};
export default Task;
Step 3: Identify The Minimal (but complete) Representation Of UI State
To add interactivity to our app we must create pieces of state to the data model.
The data in our app are:
- The list of dates we pass to DateList component
- The new date we get from user input
- The error message that appears when the user enters a date that already exists
- The error message that appears when the user removes a date selected on input and submits the empty date
- The card-date that is shown as title on the card
- The card-text the user has entered on the task textbox
- The card-task that appears as a paragraph after "add task" submission
Now we have to go through three questions to find out with piece of data is considered state
- Is it passed in from a parent via props? If so, it probably isn’t state.
- Does it remain unchanged over time? If so, it probably isn’t state.
- Can you compute it based on any other state or props in your component? If so, it isn’t state.
The list of dates and the new date we get from user are changing over time and cannot be computed based on any other state or props, therefore will be state.
The error messages are changing over time and we could compute them from 'dates' props and 'date' state inside the render method. However, we want the errors to appear only on submission and not on every page re-rendering, so we treat them as pieces of state.
The card-date changes over time but it can be computed from the 'dates' state so it's not state.
The card-text is state because it's changing over time and cannot be computed based on any other state or props.
The card-task is changing over time. Although it can be computed from the 'value' state we need to show the text in the paragraph only on user submission, hence we should treat it as state.
Finally, our state is:
- The list of dates
- The new date from the user input
- The same date error message
- The empty date error message
- The value of the textbox in the card
- The task that is passed as a paragraph in the card
Step 4: Identify Where Your State Should Live
For each piece of state in our application:
- Identify every component that renders something based on that state.
- Find a common owner component (a single component above all the components that need the state in the hierarchy).
- Either the common owner or another component higher up in the hierarchy should own the state.
- If we can’t find a component where it makes sense to own the state, create a new component solely for holding the state and add it somewhere in the hierarchy above the common owner component.
dates
:
The DateList component renders the 'dates'. The AddDate Component shows the error message based on whether the 'dates' already include the date inserted by the user. In order for both of these components to access the 'dates' piece of state, we need to move the 'dates' state to their parent - common owner component which is the App component.
date
:
This piece of state lives in the AddDate Component because that's the component where the user picks a date and we want to control the behaviour of the input.
dateExists / dateEmpty
:
These pieces of state should live in the AddDate Component because that's the component that will have to show an error message if this date already exists or if the date field is empty.
value
:
This piece of state lives in the Date component because that's the component where the user enters the text and we want to control the behavior of this input.
task
:
This piece of state lives in the Date component because that's the component where we can grab the user's text and pass it down to the Task component.
App
import React, { Component } from 'react';
import './App.css';
import AddDate from './AddDate';
import DateList from './DateList';
class App extends Component {
state = {
dates: [],
};
render() {
const dates = ['2018-04-23', '2019-06-13', '2014-09-29'];
return (
<div className="App">
<header className="App-header">
<h1>Time Machine</h1>
</header>
<AddDate dates={dates} />
<DateList dates={dates} />
</div>
);
}
}
export default App;
AddDate
import React, { Component } from 'react';
class AddDate extends Component {
state = {
date: new Date().toISOString().split('T')[0],
dateExists: false,
dateEmpty: false,
};
render() {
return (
<div className="App__form">
<form onSubmit={this.handleFormSubmit} className="App__form--date">
<div className="App__form--body">
<label>Choose Your Past:</label>
<input type="date" max={new Date().toISOString().split('T')[0]} />
</div>
<div className="App__form--btn">
<button type="submit">Add Date</button>
</div>
</form>
</div>
);
}
}
export default AddDate;
DateList
import React, { Component } from 'react';
import Date from './Date';
class DateList extends Component {
render() {
const { dates } = this.props;
return (
<div className="App__list">
<h2 className="App__list--title">Missions</h2>
<ul className="App__list--items">
{dates.map((date) => (
<Date date={date} key={date} />
))}
</ul>
</div>
);
}
}
export default DateList;
Date
import React, { Component } from 'react';
import Task from './Task';
class Date extends Component {
state = {
value: '',
task: '',
};
render() {
const { date } = this.props;
return (
<li>
<div className="App__card--inner">
<h2>{date}</h2>
<form onSubmit={this.handleFormSubmit} className="App__card">
<div className="App__card--form">
<label>Add Your Task</label>
<textarea
rows="3"
cols="30"
placeholder="type here..."
required
></textarea>
</div>
<div className="App__card--btn">
<button type="submit">Add Task</button>
</div>
</form>
<Task task={this.state.task} />
</div>
</li>
);
}
}
export default Date;
Task
import React from 'react';
const Task = (props) => {
return (
<div className="App__task">
<h3>Task</h3>
<p>{props.task}</p>
</div>
);
};
export default Task;
Step 5: Add Inverse Data Flow
In this step we want to access data the other way around: from child to parent component. Components should only update their own state so when a user adds a new date on the AddDate component it cannot have access directly to the dates state inside the App component. The way we can have access is by passing a callback from App to AddDate that will get triggered when the state should be updated. The onAddDate callback will be passed as a prop to AddDate component and when a new date is added, the callback runs and a new date is passed to the App component.
App
import React, { Component } from 'react';
import './App.css';
import AddDate from './AddDate';
import DateList from './DateList';
class App extends Component {
state = {
dates: [],
};
addDate = (date) => {
this.setState((currState) => ({
dates: [...currState.dates, date],
}));
};
render() {
return (
<div className="App">
<header className="App-header">
<h1>Time Machine</h1>
</header>
<AddDate dates={this.state.dates} onAddDate={this.addDate} />
<DateList dates={this.state.dates} />
</div>
);
}
}
export default App;
AddDate
import React, { Component } from 'react';
class AddDate extends Component {
state = {
date: new Date().toISOString().split('T')[0],
dateExists: false,
dateEmpty: false,
};
sameDateExists = (currDate) => {
const dates = this.props.dates;
for (let date of dates) {
if (date === currDate) {
return true;
}
}
return false;
};
handleFormSubmit = (event) => {
event.preventDefault();
const dateExists = this.sameDateExists(this.state.date);
if (!dateExists && this.state.date) {
this.props.onAddDate(this.state.date);
this.setState({ dateEmpty: false });
}
if (!this.state.date) {
this.setState({ dateEmpty: true });
}
if (dateExists) {
this.setState({ dateEmpty: false });
}
this.setState({ dateExists });
};
handleDateChange = (event) => {
const { value } = event.target;
this.setState((currState) => ({
...currState,
date: value,
}));
};
render() {
return (
<div className="App__form">
<form onSubmit={this.handleFormSubmit} className="App__form--date">
<div className="App__form--body">
<label>Choose Your Past:</label>
<input
type="date"
max={new Date().toISOString().split('T')[0]}
onChange={this.handleDateChange}
/>
</div>
<div className="App__form--btn">
<button type="submit">Add Date</button>
</div>
</form>
{this.state.dateExists ? (
<p className="App__form--error">This date has already been chosen</p>
) : (
''
)}
{this.state.dateEmpty ? (
<p className="App__form--error">Please choose a date</p>
) : (
''
)}
</div>
);
}
}
export default AddDate;
DateList
import React, { Component } from 'react';
import Date from './Date';
class DateList extends Component {
render() {
const { dates } = this.props;
return (
<div className="App__list">
<h2 className="App__list--title">Missions</h2>
<ul className="App__list--items">
{dates.map((date) => (
<Date date={date} key={date} />
))}
</ul>
</div>
);
}
}
export default DateList;
Date
import React, { Component } from 'react';
import Task from './Task';
class Date extends Component {
state = {
value: '',
task: '',
};
handleFormSubmit = (event) => {
event.preventDefault();
this.setState({
task: this.state.value,
});
};
handleAddTask = (event) => {
this.setState({
value: event.target.value,
});
};
render() {
const { date } = this.props;
return (
<li>
<div className="App__card--inner">
<h2>{date}</h2>
<form onSubmit={this.handleFormSubmit} className="App__card">
<div className="App__card--form">
<label>Add Your Task</label>
<textarea
rows="3"
cols="30"
placeholder="type here..."
value={this.state.value}
onChange={this.handleAddTask}
required
></textarea>
</div>
<div className="App__card--btn">
<button type="submit">Add Task</button>
</div>
</form>
<Task task={this.state.task} />
</div>
</li>
);
}
}
export default Date;
Task
import React from 'react';
const Task = (props) => {
return (
<div className="App__task">
<h3>Task</h3>
<p>{props.task}</p>
</div>
);
};
export default Task;
Finish line
Now we have a guidance on how we can break our UI into tiny pieces and then create different versions. One static version that simply takes our data model and renders the UI and the final version where interactivity is added.
I hope you had fun following this tutorial on building a react app!
You can find the code for this app here.
The app is also up and running here
Thanks for reading!
Posted on June 2, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.