Aware that this is a very common exercise; but I'm getting a bit obsessed about creating a simple, clean-ui todo app, at least for myself.
It's not quite finished yet, but I guess it will be much easier to review now than at the end, in case you look at the javascript code (react)
I'm looking any simple advice, best practice or error in the javascript-react file, or in the css html too.
function H1(){
let hello = "Hi,"
let h1 = (
<h1>
{hello}
</h1>
)
return h1
}
class Todos extends React.Component{
constructor(){
super()
this.state={todos:[]}
//todos will be an array of objects
this.addTodo = this.addTodo.bind(this)
this.toggleCompleted = this.toggleCompleted.bind(this)
}
addTodo(){
//press go -> take input value
let theValue = document.getElementById('input').value
//push array w value and id
this.setState(s => s.todos.push({id:s.todos.length, value:theValue, completed:false}))
return 1
}
toggleCompleted(e){
//toggle completed value in the state object
let parentId = e.target.parentElement.id
let completed = this.state.todos[parentId].completed
if (completed){
this.setState(s => s.todos[parentId].completed=false)
}
else {
this.setState(s => s.todos[parentId].completed=true)
}
}
render() {
let listOfTodos = ""
if (this.state.todos !== []){
listOfTodos = this.state.todos.map(todo => <TodoItem toggler={this.toggleCompleted} key={todo.id} thisItem={todo}/>)
}
return (
<div className="todos__area">
<div className="todos__events">
<input id='input' className="todos__input" placeholder="type here" type="text" />
<button onClick = {this.addTodo} className="todos__button">Go</button>
</div>
<ul className="todos__list">
{listOfTodos}
</ul>
</div>
)
}
}
function TodoItem(props){
return (<li id={props.thisItem.id}>
<input onClick={props.toggler} className="checkbox" type='checkbox'/>
<span style=
{{textDecoration:props.thisItem.completed?"line-through":"none"}}>
{props.thisItem.value}
</span> </li>)
}
function Footer(){
return (
<footer>
<ul className="footer__ul pointer">
<li>Github</li>
<li>Reddit</li>
<li>FCC</li>
</ul>
</footer>
)
}
function App(){
//render components
return (
<main>
<H1 />
<Todos />
<Footer />
</main>
)
}
ReactDOM.render(<App/>, document.getElementById('root'))
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
:root {
--blue:#2c3251;
}
ul {
list-style-type: none;
}
ul > li {
display: block;
padding: 0.5rem;
}
.pointer > li:hover {
cursor: pointer;
}
body {
font-family: "Ubuntu", sans-serif;
text-align: left;
background-color: white;
background-image: radial-gradient(rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.6)), url("./table.png");
background-repeat: repeat;
padding: 2rem 2rem 0.1rem;
}
main {
display: grid;
min-height: auto;
height: 100vh;
width: 100%;
}
h1 {
font-size: 1.8rem;
color: var(--blue);
box-shadow: inset 0px 2px 5px var(--blue);
align-self: start;
padding: 1.5rem;
border-radius: 15px 15px 0 0;
margin-bottom: 0.5rem;
}
.todos__area {
box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.7), 1px 1px 5px rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
max-height: 300px;
}
.todos__events {
display: flex;
flex-direction: row;
flex: 0 1 40px;
}
.todos__events .todos__input {
border: none;
padding: 0.5rem;
border-radius: 2px 0 0 2px;
flex: 2 1 150px;
}
.todos__events .todos__button {
border: none;
padding: 10px;
flex: 1 1 60px;
color: white;
font-weight: bold;
cursor: pointer;
max-width: 100px;
position: relative;
background-color: rgba(0, 0, 0, 0);
}
.todos__events .todos__button:after {
z-index: -1;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #2d7b57;
opacity: 0.5;
transition: opacity 0.3s ease-out;
content: "";
}
.todos__events .todos__button:hover:after {
opacity: 0.7;
}
/* Inline #1 | http://localhost:3000/ */
.todos__list {
font-family: "Dancing Script", "cursive";
font-size: 1.2rem;
padding: 2rem;
background-color: rgba(227, 227, 134, 0.58);
flex: 1 1 200px;
overflow: auto;
}
.todos__list input[type=checkbox] {
margin-right: 0.6rem;
}
footer {
background-color: rgba(0, 0, 0, 0.1);
align-self: end;
margin-top: 0.5rem;
}
.footer__ul {
display: flex;
justify-content: center;
color: var(--blue);
font-weight: bold;
}
.footer__ul > li {
padding: 1rem;
box-shadow: 1px 0px 1px var(--blue);
transition: transform 0.1s ease-out;
}
.footer__ul > li:hover {
transform: scaleX(1.05);
}
@media screen and (min-width: 700px) {
body {
max-width: 700px;
margin: auto;
}
}
/*# sourceMappingURL=index.css.map */
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<!DOCTYPE html>
<html lang="en">
<link href="https://fonts.googleapis.com/css2?family=Dancing+Script&family=Ubuntu&display=swap" rel="stylesheet">
<head>
<meta charset="utf-8" />
<title>React App: todoist</title>
</head>
<body >
<div id="root"></div>
</body>
</html>
A few things yet to add:
- "delete button"
- "done" button (and probably remove the checkbox)
- color scheme and welcome message ("Hi") changing with the time of the day.
- the "go" button, should work by hitting enter/return too. I'm not sure how to do that so far.
Hope you like :-)
1 Answer 1
You've several issues with state mutation, which is a major anti-pattern in react, poor variable declarations, and other sub-optimal coding style.
constructor
- The constructor is missing props (and passing them to
super
). It doesn't appear that currently any props are passed toTodos
, but should this ever change it could cause issue later on down the road. Better to just assume some props can be passed.
updates
constructor(props) {
super(props);
this.state={
todos: [],
};
...
}
addToDo
addTodo(){
//press go -> take input value
let theValue = document.getElementById('input').value;
//push array w value and id
this.setState(s => s.todos.push({id:s.todos.length, value: theValue, completed:false}));
return 1;
}
theValue
never changes so it should be declared const.- Pushing into an array mutates the existing array and returns the new length, so not only did this mutate existing state, it updated it to no longer be an array. Use a correct functional state update that shallowly copies previous state.
addToDo
is used as anonClick
handler, so the return is meaningless.document.getElementById
is generally also considered an anti-pattern. Using a ref is the more "react way" of getting the value
User Experience (UX): After finishing a code review I ran your code snippet and noticed the input doesn't clear after being added. Just a suggestion here, but maybe clear out the input field upon adding a todo item.
updates
constructor() {
super();
...
this.inputRef = React.createRef(); // <-- create input ref
...
}
addTodo(){
// press go -> take input value
const { value } = this.inputRef.current;
// shallow copy existing state and append new value
this.setState(({ todos }) => ({
todos: [...todos, { id: todos.length, value, completed: false }],
}));
// Suggestion to clear input
this.inputRef.current.value = '';
}
...
<input
ref={this.inputRef} // <-- attach inputRef to input
id='input'
className="todos__input"
placeholder="type here"
type="text"
/>
...
toggleCompleted
toggleCompleted(e){
//toggle completed value in the state object
let parentId = e.target.parentElement.id
let completed = this.state.todos[parentId].completed
if (completed){
this.setState(s => s.todos[parentId].completed=false)
} else {
this.setState(s => s.todos[parentId].completed=true)
}
}
- Variables
parentId
andcompleted
don't change, so should also be declared const. - Similar issue with state mutation. You still need to shallowly copy the existing state and update the element by index/id.
- The two logic branches of
if (completed)
are nearly identical, a more DRY approach would be to do the branching at the value, i.e. using a ternary, or even better, just simply toggle the boolean value, like the function's name suggests. - Get the id of the target element of the event object (more on this later)
updates
toggleCompleted(e){
// toggle completed value in the state object
const { id } = e.target;
this.setState(({ todos }) => ({
todos: todos.map(todo => todo.id === Number(id) ? { // <-- id is string
...todo,
completed: !todo.completed,
} : todo),
}));
}
render
- So long as
this.state.todos
is an array, the map function can correctly handle an empty array, no need really to test that it isn't equal to an empty array ([]
), but if there is concern it is more common to conditionally render with a check on the length, i.e.this.state.todos.length && this.state.todos.map(...
.
updates
render() {
return (
<div className="todos__area">
<div className="todos__events">
<input ref={this.inputRef} id='input' className="todos__input" placeholder="type here" type="text" />
<button onClick={this.addTodo} className="todos__button">Go</button>
</div>
<ul className="todos__list">
{this.state.todos.map(todo => (
<TodoItem
key={todo.id}
toggler={this.toggleCompleted}
thisItem={todo}
/>
))}
</ul>
</div>
)
}
TodoItem
- The input checkbox should probably use the
onChange
handler versus anonClick
, it's semantically more correct. - Attach the
id
to the input instead of the parent node. - Set the
checked
value of the input to the item completed status. - Wrap the input and span in a label so it can be clicked on to toggle the completed state.
updates
function TodoItem({ thisItem, toggler }){
return (
<li>
<label>
<input
id={thisItem.id}
checked={thisItem.completed}
className="checkbox"
onChange={toggler}
type='checkbox'
/>
<span
style={{
textDecoration: thisItem.completed ? "line-through" : "none"
}}
>
{thisItem.value}
</span>
</label>
</li>
)
}
Running Demo
function H1() {
let hello = "Hi,";
let h1 = <h1> {hello} </h1>;
return h1;
}
class Todos extends React.Component {
constructor() {
super();
this.state = {
todos: []
};
this.inputRef = React.createRef(); // <-- create input ref
//todos will be an array of objects
this.addTodo = this.addTodo.bind(this);
this.toggleCompleted = this.toggleCompleted.bind(this);
}
addTodo() {
// press go -> take input value
const { value } = this.inputRef.current;
// shallow copy existing state and append new value
this.setState(({ todos }) => ({
todos: [
...todos,
{
id: todos.length,
value,
completed: false
}
]
}));
// Suggestion to clear input
this.inputRef.current.value = "";
}
toggleCompleted(e) {
// toggle completed value in the state object
const { id } = e.target;
this.setState(({ todos }) => ({
todos: todos.map(todo =>
todo.id === Number(id)
? {
...todo,
completed: !todo.completed
}
: todo
)
}));
}
render() {
return (
<div className="todos__area">
<div className="todos__events">
<input
ref={this.inputRef}
id="input"
className="todos__input"
placeholder="type here"
type="text"
/>
<button onClick={this.addTodo} className="todos__button">
{" "}
Go{" "}
</button>{" "}
</div>{" "}
<ul className="todos__list">
{" "}
{this.state.todos.map(todo => (
<TodoItem
key={todo.id}
toggler={this.toggleCompleted}
thisItem={todo}
/>
))}{" "}
</ul>{" "}
</div>
);
}
}
function TodoItem({ thisItem, toggler }) {
return (
<li>
<label>
<input
id={thisItem.id}
checked={thisItem.completed}
className="checkbox"
onChange={toggler}
type="checkbox"
/>
<span
style={{
textDecoration: thisItem.completed ? "line-through" : "none"
}}
>
{thisItem.value}{" "}
</span>{" "}
</label>{" "}
</li>
);
}
function Footer() {
return (
<footer>
<ul className="footer__ul pointer">
<li> Github </li> <li> Reddit </li> <li> FCC </li>{" "}
</ul>{" "}
</footer>
);
}
function App() {
//render components
return (
<main>
<H1 />
<Todos />
<Footer />
</main>
);
}
ReactDOM.render( < App / > , document.getElementById('root'))
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
:root {
--blue: #2c3251;
}
ul {
list-style-type: none;
}
ul>li {
display: block;
padding: 0.5rem;
}
.pointer>li:hover {
cursor: pointer;
}
body {
font-family: "Ubuntu", sans-serif;
text-align: left;
background-color: white;
background-image: radial-gradient(rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.6)), url("./table.png");
background-repeat: repeat;
padding: 2rem 2rem 0.1rem;
}
main {
display: grid;
min-height: auto;
height: 100vh;
width: 100%;
}
h1 {
font-size: 1.8rem;
color: var(--blue);
box-shadow: inset 0px 2px 5px var(--blue);
align-self: start;
padding: 1.5rem;
border-radius: 15px 15px 0 0;
margin-bottom: 0.5rem;
}
.todos__area {
box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.7), 1px 1px 5px rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
max-height: 300px;
}
.todos__events {
display: flex;
flex-direction: row;
flex: 0 1 40px;
}
.todos__events .todos__input {
border: none;
padding: 0.5rem;
border-radius: 2px 0 0 2px;
flex: 2 1 150px;
}
.todos__events .todos__button {
border: none;
padding: 10px;
flex: 1 1 60px;
color: white;
font-weight: bold;
cursor: pointer;
max-width: 100px;
position: relative;
background-color: rgba(0, 0, 0, 0);
}
.todos__events .todos__button:after {
z-index: -1;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #2d7b57;
opacity: 0.5;
transition: opacity 0.3s ease-out;
content: "";
}
.todos__events .todos__button:hover:after {
opacity: 0.7;
}
/* Inline #1 | http://localhost:3000/ */
.todos__list {
font-family: "Dancing Script", "cursive";
font-size: 1.2rem;
padding: 2rem;
background-color: rgba(227, 227, 134, 0.58);
flex: 1 1 200px;
overflow: auto;
}
.todos__list input[type=checkbox] {
margin-right: 0.6rem;
}
footer {
background-color: rgba(0, 0, 0, 0.1);
align-self: end;
margin-top: 0.5rem;
}
.footer__ul {
display: flex;
justify-content: center;
color: var(--blue);
font-weight: bold;
}
.footer__ul>li {
padding: 1rem;
box-shadow: 1px 0px 1px var(--blue);
transition: transform 0.1s ease-out;
}
.footer__ul>li:hover {
transform: scaleX(1.05);
}
@media screen and (min-width: 700px) {
body {
max-width: 700px;
margin: auto;
}
}
/*# sourceMappingURL=index.css.map */
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<!DOCTYPE html>
<html lang="en">
<link href="https://fonts.googleapis.com/css2?family=Dancing+Script&family=Ubuntu&display=swap" rel="stylesheet">
<head>
<meta charset="utf-8" />
<title>React App: todoist</title>
</head>
<body>
<div id="root"></div>
</body>
</html>