Untitled

mail@pastecode.io avatar
unknown
tsx
3 years ago
3.1 kB
3
Indexable
Never
import React, { useEffect, useMemo, useState } from 'react';

type Props = {
  todos?: TodoItem[];
};

type TodoItem = {
  id: string | number;
  title: string;
  isDone?: boolean;
};

const App: React.FC<Props> = (props) => {
  return (
    <div>
      <NotTouchingTheTree todos={props.todos} />
      <ExpensiveTree />
    </div>
  );
};

// We rerender this whole thing no matter what. If we want to improve performance, we can only separate the state from ExpensiveTree or memoize it
const NotTouchingTheTree: React.FC<Props> = (props) => {
  const [todos, setTodos] = useState<TodoItem[]>(props.todos ?? []);
  const [filter, setFilter] = useState<'all' | 'undone'>('all');

  useEffect(() => {
    if (props.todos?.length) {
      setFilter('all'); // did we want to reset filter on props change here?
    }
  }, [props.todos]);

  const addTodo = () => {
    const title = prompt('What to do?');

    if (title) {
      setTodos((todos) => {
        return [...todos, { id: Date.now(), title }];
      });
    }
  };

  const markAsDone = (todo: TodoItem) => {
    const index = todos.findIndex((item) => item.id === todo.id);

    if (index >= 0) {
      const res = updateAt(todos, index, { ...todo, isDone: true });
      setTodos(res);
    }
  };

  const markAsUndone = (todo: TodoItem) => {
    const index = todos.findIndex((item) => item.id === todo.id);

    if (index >= 0) {
      const res = updateAt(todos, index, { ...todo, isDone: false });
      setTodos(res);
    }
  };

  const deleteTodo = (todo: TodoItem) => {
    setTodos((todos) => todos.filter((item) => item.id !== todo.id));
  };

  const onFilterButtonClick = () => {
    setFilter((filter) => (filter === 'all' ? 'undone' : 'all'));
  };

  const filteredTodos = useMemo(() => {
    return todos.filter((todo) => (filter === 'undone' ? !todo.isDone : true));
  }, [todos, filter]);

  const doneCount = useMemo(() => {
    return todos.reduce((count, todo) => count + (todo.isDone ? 1 : 0), 0);
  }, [todos]);

  return (
    <>
      <p>{`${doneCount} / ${todos.length}`}</p>
      <ul>
        {filteredTodos.map((todo) => (
          <div key={`${todo.id}`}>
            <p>{`${todo.isDone ? '✅ ' : ''}${todo.title}`}</p>
            <button
              onClick={() => {
                todo.isDone ? markAsUndone(todo) : markAsDone(todo);
              }}
            >
              Done
            </button>
            <button onClick={() => deleteTodo(todo)}>Delete</button>
          </div>
        ))}
      </ul>
      {/* we can useCallback on these but there is no reason for it with plain DOM elements */}
      <button onClick={addTodo}>Add</button>
      <button onClick={onFilterButtonClick}>
        {`Show ${filter === 'all' ? 'undone' : 'all'} todos`}
      </button>
    </>
  );
};

function updateAt<T>(arr: T[], index: number, value: T): T[] {
  return [...arr.slice(0, index), value, ...arr.slice(index + 1)];
}

function ExpensiveTree() {
  let now = performance.now();

  while (performance.now() - now < 1000) {
    // Artificial delay -- do nothing for 1000ms
  }

  return null;
}

export default App;