aurel kurtula
Posted on December 24, 2017
After having explored the basics of firebase and react, I thought I'd use them all together in this tutorial. In this three part series, I am going to create another todo app. I'm going to use react, the basics of which I covered here where I made a simpler version of the same app. I'm also going to use react routing, which I also covered in this post.
Since I don't want this tutorial to be very long, I'm going to add firebase to this project in part two. In that tutorial, we'll move the data from our react component state to the firebase database. Then in part three we'll add authentication where users can add their own private todo items.
Create the static markup
First we'll quickly create the basic design of the app. Everything I'll do here I have already covered else where. Let's start by installing the package we need for routing in react.
yarn add react-router-dom
The App
component is going to be the main component. It will hold the state and the logic of the application. However, let's start by creating the basic structure. If you want to start in codesandbox that means start editing in index.js
. If you create a react application through the terminal, you start in src/App
.
import React, {Component} from 'react'
import { BrowserRouter, Route, Link } from 'react-router-dom';
import './App.css'
class App extends Component {
state = {
items: {
1123: {
item: 'item one',
completed: false
},
2564321: {
item: 'item two',
completed: true
}
}
}
render() {
return (
<BrowserRouter>
<div className="wrap">
<h2>A simple todo app</h2>
<ul className="menu">
<li><Link to={'/'}>To do</Link></li>
<li><Link to={'/completed'}>Completed</Link></li>
</ul>
<Route exact path="/" render={props => {
let lis = []
for(let i in this.state.items){
if(this.state.items[i].completed === false){
lis.push(<li key={i}>{this.state.items[i].item} <span >x</span></li>)
}
}
return(<ul className="items"> { lis } </ul> )
}
} />
<Route exact path="/completed" render={props => {
let lis = []
for(let i in this.state.items){
if(this.state.items[i].completed === true){
lis.push(<li key={i}>{this.state.items[i].item} <span >x</span></li>)
}
}
return(<ul className="items"> { lis } </ul> )
}
} />
</div>
</BrowserRouter>
);
}
}
export default App;
When loading the app in your browser, you'll be able to navigate between the homepage and /completed
and see the difference.
For an explenation on how the above code works, read my previous tutorial on the basics of React Router
Using child components
Let's create a child component which will take care of the duplicate code. Create a file at components/ItemsComponent.js
and add the following code.
import React from 'react'
const ItemsComponent=({items, done})=> {
let lis = []
let mark = done === false ? '\u2713' : 'x';
for(let i in items){
if(items[i].completed === done){
lis.push(<li key={i}>{items[i].item} <span >{mark}</span></li>)
}
}
return(<ul className="items"> {lis} </ul> )
}
export default ItemsComponent;
That is a stateless functional component, as you can see, it doesn't need a class (a shout out to @omensah for nudging me on this direction). It's perfect for cases like these, where the logic doesn't require to make use of functionality that we'd otherwise inherit from Component
class. Cory House has perfectly compared the two styles in this post
Let's modify the App
component to make use of ItemsComponent
which will also clarify the deconstructed arguments in line 2.
import ItemsComponent from './components/ItemsComponent';
class App extends Component {
..
return (
<BrowserRouter>
<div className="wrap">
...
<Route exact path="/"
render={props =>
<ItemsComponent items={this.state.items} done={false}/>
}/>
<Route exact path="/completed"
render={props =>
<ItemsComponent items={this.state.items} done={true}/>
}/>
</div>
</BrowserRouter>
);
}
}
export default App;
We render the ItemsComponent
component using render
rather than using the component
attribute, which I covered when writing about react routers because we needed to pass it the items a boolian to signal which items to display. With that the use of ES6 deconstruction is self explanatory:
const ItemsComponent=({items, done})=> { ... }
The above could have otherwise be writen as
const ItemsComponent=(props)=> { ... }
Which we would have had to then reach in the props
object to retrieve items
or done
.
Adding actions
The first two actions that we'll work on are the ability to mark an item as complete, and also completely delete any completed item.
As I said the App
component is going to be the main component. It holds our main state. So let's write the methods that modifies the state.
class App extends Component {
state = {
items: {
1123: {
item: 'item one',
completed: false
},
2564321: {
item: 'item two',
completed: true
}
}
}
completeItem=(id)=>{
let items = {
...this.state.items,
[id]: {...this.state.items[id], completed: true }
}
this.setState({ items })
}
deleteItem = (id) => {
let {[id]: deleted, ...items} = this.state.items;
this.setState({ items })
}
...
completeItem
method takes the items from the current state, then we select the item with the relevant id
, and finally change its completed
property to true
.
Deleting the relevant object is slightly different. I'm currently trying to learn more about the spread operator and that's why I added it above. I found the snippet ... guess where? ... at stackoverflow
Next, completeItem
and deleteItem
methods need to be passed to the ItemsComponent
render() {
return (
...
<Route exact path="/"
render={props =>
<ItemsComponent
items={this.state.items}
done={false}
action={this.completeItem}
/>
}/>
<Route exact path="/completed"
render={props =>
<ItemsComponent
items={this.state.items}
done={true}
action={this.deleteItem}
/>
}/>
...
)
Finally we just strap action
to an onClick
event over at components/itemsComponent.js
const ItemsComponent=({items, done, action})=> {
let lis = []
let mark = done === false ? '\u2713' : 'x';
for(let i in items){
if(items[i].completed === done){
lis.push(<li key={i}>{items[i].item}
<span onClick={()=> action(i)}>{mark}</span></li>)
}
}
return(<ul className="items"> {lis} </ul> )
}
Note, the only thing that's changed is the deconstruction of the action
method in the first line. Then I added it to the span. i
is the id of each object within the items
object.
Adding items
A todo Application is no good if users can't add items. At the moment, the item's are hard coded, but that was to help us get to this point.
The way this will work is that I want users to be able to add new items only when thay are viewing the uncompleted items, in other words, only when they are at the root path and not the /completed
path. Let's add the input box inside the components/ItemsComponent.js
file:
const ItemsComponent=({items, done, action})=> {
...
return (
<div>
{done
? (<ul className="items"> {lis} </ul>)
: (
<div>
<form>
<input type="text" />
</form>
<ul className="items"> {lis} </ul>
</div>
)}
</div>
);
}
Remember, done
is a boolian, if true
it means the items are marked as completed, hence we do not want to see the form, else, we do.
React requires the outer div to wrap the entire output, and it also requires the form
and ul
to be wrapped with an element.
Finally, just as with the delete and complete operations, we'll add the form's logic at App
components and link it via props with the form. Let's create the logic in App.js
class App extends Component {
...
addItem=(e)=> {
e.preventDefault();
let items = {
...this.state.items,
[new Date().valueOf()]: {
item: this.todoItem.value,
completed: false
}
}
this.setState({
items
});
}
render() {
return (
...
<Route exact path="/"
render={props =>
<ItemsComponent
...
addItem={this.addItem}
inputRef={el => this.todoItem = el}
/>
}/>
...
);
}
}
addItem
will execute on form submit. Then it simply adds an item to the state. new Date().valueOf()
is a basic way of creating a unique id. this.todoItem.value
is created from the inputRef
attribute that we created in ItemsComponent
. You can read more about Refs (as they are called) in the documentation
Now let's use addItem
and inputRef
in the form over at ItemsComponent.js
.
const ItemsComponent=({items, done, action, addItem, inputRef})=> {
...
return (
...
<form onSubmit={addItem}>
<input ref={inputRef} type="text" />
</form>
<ul className="items"> {lis} </ul>
...
);
}
We attach the input
node as a reference to inputRef
(which is passed through props).
Conclusion
So far we have a basic react application where we can add items, mark them as complete then delete any that are completed. We also made use of routing to differentiate between the two.
The completed project can be found at github. I'll have a branch for each tutorial.
The next tutorial is going to connect the react state with the Firebase database.
Posted on December 24, 2017
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 27, 2024