Como empezar?
Antes que nada, vamos a empezar creando un nuevo CodeSandbox.
Vamos a descomponer la Todo App en 4 componentes:
- Layout
- AddTodo
- TodoListItem
- TodoList
Como no queremos que todos estos componentes se re-rendericen a menos que sea realmente necesario, los vamos a envolver usando el helper memo de React.
Desarrollemos el Layout
Nuestro componente Layout solo va a renderizar un Toolbar con el nombre de nuestra App y va a poder recibir componentes hijos.
import React, { memo } from 'react';
import { AppBar, Toolbar, Typography, Paper } from '@material-ui/core';
const Layout = memo(props => (
<Paper
elevation={0}
style={{ padding: 0, margin: 0, backgroundColor: '#fafafa' }}
>
<AppBar color="primary" position="static" style={{ height: 64 }}>
<Toolbar style={{ height: 64 }}>
<Typography color="inherit">TODO APP</Typography>
</Toolbar>
</AppBar>
{props.children}
</Paper>
));
export default Layout;
Desarrollemos el AddTodo
Nuestro AddTodo va a renderizar un componente Paper envolviendo un input y un boton, ambos van a recibir event handlers pasados como props desde un componente padre.
import React, { memo } from 'react';
import { TextField, Paper, Button, Grid } from '@material-ui/core';
const AddTodo = memo(props => (
<Paper style={{ margin: 16, padding: 16 }}>
<Grid container>
<Grid xs={10} md={11} item style={{ paddingRight: 16 }}>
<TextField
placeholder="Add Todo here"
value={props.inputValue}
onChange={props.onInputChange}
onKeyPress={props.onInputKeyPress}
fullWidth
/>
</Grid>
<Grid xs={2} md={1} item>
<Button
fullWidth
color="secondary"
variant="outlined"
onClick={props.onButtonClick}
>
Add
</Button>
</Grid>
</Grid>
</Paper>
));
export default AddTodo;
Desarrollemos el TodoListItem
Nuestro componente TodoListItem va a renderizar un ListItem envolviendo un checkbox, un contenedor de texto y un boton como elementos hijos.
import React, { memo } from 'react';
import {
List,
ListItem,
Checkbox,
IconButton,
ListItemText,
ListItemSecondaryAction,
} from '@material-ui/core';
import DeleteOutlined from '@material-ui/icons/DeleteOutlined';
const TodoListItem = memo(props => (
<ListItem divider={props.divider}>
<Checkbox
onClick={props.onCheckBoxToggle}
checked={props.checked}
disableRipple
/>
<ListItemText primary={props.text} />
<ListItemSecondaryAction>
<IconButton aria-label="Delete Todo" onClick={props.onButtonClick}>
<DeleteOutlined />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
));
export default TodoListItem;
Desarrollemos el TodoList
Nuestro componente TodoList va a renderizar solo cuando haya un item agregado a la lista. Va a estar a cargo de renderizar los componentes TodoListItem.
import React, { memo } from 'react';
import { List, Paper } from '@material-ui/core';
import TodoListItem from './TodoItem';
const TodoList = memo(props => (
<>
{props.items.length > 0 && (
<Paper style={{ margin: 16 }}>
<List style={{ overflow: 'scroll' }}>
{props.items.map((todo, idx) => (
<TodoListItem
{...todo}
key={`TodoItem.${idx}`}
divider={idx !== props.items.length - 1}
onButtonClick={() => props.onItemRemove(idx)}
onCheckBoxToggle={() => props.onItemCheck(idx)}
/>
))}
</List>
</Paper>
)}
</>
));
export default TodoList;
Pasemos a la logica
Bueno, loco, ya tenemos todos los componentes listos. Es momento de pensar en la logica y aca vamos a hacer toda la logica usando custom React Hooks (Hell yeah!).
Vamos a descomponer la logica de la Todo App en 2 variantes:
- Manejo de estado del input
- Manejo de estado de los todos
Desarrollemos la logica del estado del Input
El estado del input va a tener un unico valor que va a ser un string, nuestro custom hook va a retornar ese valor, y tambien, un conjunto de funciones para manejar los eventos de cambio, reset e input.
import { useState } from 'react';
export const useInputValue = (initialValue = '') => {
const [inputValue, setInputValue] = useState(initialValue);
return {
inputValue,
changeInput: event => setInputValue(event.target.value),
clearInput: () => setInputValue(''),
keyInput: (event, callback) => {
if (event.which === 13 || event.keyCode === 13) {
callback(inputValue);
return true;
}
return false;
},
};
};
Desarrollemos la logica del estado de los Todos
El estado de los todos va a tener un unico valor que va a ser un array, nuestro custom hook va a retornar ese valor, y tambien, un conjunto de funciones custom para manejar el agregado, el checkeo y la eliminacion de todos.
import { useState } from 'react';
export const useTodos = (initialValue = []) => {
const [todos, setTodos] = useState(initialValue);
return {
todos,
addTodo: text => {
if (text !== '') {
setTodos(
todos.concat({
text,
checked: false,
})
);
}
},
checkTodo: idx => {
setTodos(
todos.map((todo, index) => {
if (idx === index) {
todo.checked = !todo.checked;
}
return todo;
})
);
},
removeTodo: idx => {
setTodos(todos.filter((todo, index) => idx !== index));
},
};
};
Conectando todas las piezas
Tenemos los componentes, y tambien la logica. Mezclemos todo para hacer que nuestra App funcione:
import './styles.css';
import React, { memo } from 'react';
import ReactDOM from 'react-dom';
import { useInputValue, useTodos } from './custom-hooks';
import Layout from './components/Layout';
import AddTodo from './components/AddTodo';
import TodoList from './components/TodoList';
const TodoApp = memo(props => {
const { inputValue, changeInput, clearInput, keyInput } = useInputValue();
const { todos, addTodo, checkTodo, removeTodo } = useTodos();
const clearInputAndAddTodo = _ => {
clearInput();
addTodo(inputValue);
};
return (
<Layout>
<AddTodo
inputValue={inputValue}
onInputChange={changeInput}
onButtonClick={clearInputAndAddTodo}
onInputKeyPress={event => keyInput(event, clearInputAndAddTodo)}
/>
<TodoList
items={todos}
onItemCheck={idx => checkTodo(idx)}
onItemRemove={idx => removeTodo(idx)}
/>
</Layout>
);
});
ReactDOM.render(<TodoApp />, document.getElementById('root'));
Miremos los resultados
Veamos nuestra Todo App andando: