티스토리 뷰

☀️ React-Next.js

To Do - React 예제 코드

James Wetzel 2025. 1. 4. 11:28
728x90
반응형
import { atom, selector } from 'recoil';
import axios from 'axios';

export interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

// Atom for managing todos
export const todosState = atom<Todo[]>({
  key: 'todosState',
  default: [],
});

// Selector to fetch todos from server
export const todosSelector = selector<Todo[]>({
  key: 'todosSelector',
  get: async () => {
    try {
      const response = await axios.get('https://jsonplaceholder.typicode.com/todos');
      return response.data.slice(0, 10); // Limiting to 10 items for simplicity
    } catch (error) {
      console.error('Failed to fetch todos:', error);
      return [];
    }
  },
});

 

 

import React, { useState, useEffect } from 'react';
import { useRecoilState, useRecoilValueLoadable } from 'recoil';
import { todosState, todosSelector, Todo } from './recoilState';
import axios from 'axios';

function Todos() {
  const [todos, setTodos] = useRecoilState(todosState);
  const [newTitle, setNewTitle] = useState('');
  const [editingId, setEditingId] = useState<number | null>(null);
  const [editTitle, setEditTitle] = useState('');
  const [error, setError] = useState<string | null>(null);
  const todosLoadable = useRecoilValueLoadable(todosSelector);

  const API_BASE_URL = 'https://jsonplaceholder.typicode.com/todos';

  // Fetch todos from the server on initial load
  useEffect(() => {
    if (todosLoadable.state === 'hasValue' && todosLoadable.contents.length > 0) {
      setTodos(todosLoadable.contents);
    }
  }, [todosLoadable]);

  // Optimistic Add
  const handleAdd = async () => {
    if (newTitle.trim() === '') return;

    const newTodo: Todo = {
      id: Date.now(),
      title: newTitle,
      completed: false,
    };

    setTodos((prev) => [...prev, newTodo]); // Optimistic UI
    setNewTitle('');

    try {
      const response = await axios.post(API_BASE_URL, newTodo);
      setTodos((prev) =>
        prev.map((todo) => (todo.id === newTodo.id ? response.data : todo))
      );
    } catch (error) {
      console.error('Failed to create todo:', error);
      setError('Failed to add the todo. Try again.');
      setTodos((prev) => prev.filter((todo) => todo.id !== newTodo.id)); // Rollback on error
    }
  };

  // Optimistic Update
  const handleUpdate = async (id: number) => {
    const originalTodo = todos.find((todo) => todo.id === id);
    if (!originalTodo) return;

    setTodos((prev) =>
      prev.map((todo) => (todo.id === id ? { ...todo, title: editTitle } : todo))
    );

    try {
      const response = await axios.patch(`${API_BASE_URL}/${id}`, { title: editTitle });
      setTodos((prev) =>
        prev.map((todo) => (todo.id === id ? response.data : todo))
      );
    } catch (error) {
      console.error('Failed to update todo:', error);
      setError('Failed to update the todo. Try again.');
      setTodos((prev) => [...prev, originalTodo]); // Rollback on error
    } finally {
      setEditingId(null);
      setEditTitle('');
    }
  };

  // Optimistic Delete
  const handleDelete = async (id: number) => {
    const originalTodos = [...todos];

    setTodos((prev) => prev.filter((todo) => todo.id !== id)); // Optimistic UI

    try {
      await axios.delete(`${API_BASE_URL}/${id}`);
    } catch (error) {
      console.error('Failed to delete todo:', error);
      setError('Failed to delete the todo. Try again.');
      setTodos(originalTodos); // Rollback on error
    }
  };

  // Auto-sync with server every 10 seconds
  useEffect(() => {
    const interval = setInterval(async () => {
      try {
        const response = await axios.get(API_BASE_URL);
        setTodos(response.data.slice(0, 10)); // Update state with latest data
      } catch (error) {
        console.error('Failed to sync todos:', error);
      }
    }, 10000);

    return () => clearInterval(interval); // Cleanup on unmount
  }, []);

  return (
    <div>
      <h2>Todos</h2>
      {error && <p style={{ color: 'red' }}>{error}</p>}
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            {editingId === todo.id ? (
              <input
                type="text"
                value={editTitle}
                onChange={(e) => setEditTitle(e.target.value)}
              />
            ) : (
              <span>{todo.title}</span>
            )}
            <button onClick={() => handleDelete(todo.id)}>Delete</button>
            {editingId === todo.id ? (
              <button onClick={() => handleUpdate(todo.id)}>Save</button>
            ) : (
              <button onClick={() => setEditingId(todo.id)}>Edit</button>
            )}
          </li>
        ))}
      </ul>
      <input
        type="text"
        value={newTitle}
        onChange={(e) => setNewTitle(e.target.value)}
        placeholder="Add a new todo"
      />
      <button onClick={handleAdd}>Add</button>
    </div>
  );
}

export default Todos;
728x90
반응형