javascript – Should I use the Redux store to pass data from child to parent?


I am building a to-do/notes app in order to learn the basics of Redux, using React hooks and Typescript.

A note is composed of an ID and a value. The user can add, delete or edit a note.

The add / delete mechanics work fine. But the edit one is trickier for me, as I’m questionning how it should be implemented.

I think my reducer’s code is fine. The problem lies between my component (Note.tsx) and its parent one (App.tsx).

When i’m logging the value, I can see that the new updated/edited value of the note is not sent to the reducer. As a result, my note is not edited with the new value.

I’ve tried “cloning” the redux store and making my changes here, but it seems tedious and unnatural to me. Should I just call the edit method from my Note.tsx component ?

Is there a clean / conventional way to do this ?

Here is my code :

App.tsx

function App() {
  const notes = useSelector<NotesStates, NotesStates('notes')>(((state) => state.notes));

  const dispatch = useDispatch();

  const onAddNote = (note: string) => {
    dispatch(addNote(note));
  };

  const onDeleteNote = (note: NoteType) => {
    dispatch(deleteNote(note));
  };

  const onEditNote = (note: NoteType) => {
    dispatch(updateNote(note));
  };

  return (
    <div className="home">
      <NewNoteInput addNote={onAddNote} />
      <hr />
      <ul className="notes">
        {notes.map((note) => (
          <Note
            updateNote={() => onEditNote(note)}
            deleteNote={() => onDeleteNote(note)}
            note={note}
          />
        ))}
      </ul>
    </div>
  );
}

Note.tsx

interface NoteProps {
    deleteNote(): void
    updateNote(noteValue: string | number): void
    note: NoteType
}

const Note: React.FC<NoteProps> = ({ deleteNote, updateNote, note: { id, value } }) => {
  const (isEditing, setIsEditing) = useState(false);
  const (newNoteValue, setNewNoteValue) = useState(value);

  const onDeleteNote = () => {
    deleteNote();
  };

  const onUpdateNote = () => {
    updateNote(newNoteValue);
    setIsEditing(false);
  };

  const handleOnDoubleClick = () => {
    setIsEditing(true);
  };

  const renderBody = () => {
    if (!isEditing) {
      return (
        <>
          {!value && <span className="empty-text">Note is empty</span>}
          <span>{value}</span>
        </>
      );
    }
    return (
      <input
        value={newNoteValue}
        onChange={(e) => setNewNoteValue(e.target.value)}
        onBlur={onUpdateNote}
      />
    );
  };

  return (
    <li className="note" key={id}>
      <span className="note__title">
        Note n°
        {id}
      </span>

      <div className="note__body" onDoubleClick={handleOnDoubleClick}>
        {renderBody()}
      </div>
      <button type="button" onClick={onDeleteNote}>Delete</button>
    </li>
  );
};

export default Note;

and the notesReducer.tsx

export interface NotesStates {
    notes: Note()
}

export interface Note {
    id: number
    value: string
}

const initialState = {
  notes: (),
};

let noteID = 0;

export const notesReducer = (state: NotesStates = initialState, action: NoteAction): NotesStates => {
  switch (action.type) {
    case 'ADD_NOTE': {
      noteID += 1;
      return {
        ...state,
        notes: (...state.notes, {
          id: noteID,
          value: action.payload,
        }),
      };
    }
    case 'UPDATE_NOTE': {
      return {
        ...state,
        notes: state.notes.map((note) => {
          if (note.id === action.payload.id) {
            return {
              ...note,
              value: action.payload.value,
            };
          }
          return note;
        }),
      };
    }
    case 'DELETE_NOTE': {
      return {
        ...state,
        notes: (...state.notes
          .filter((note) => note.id !== action.payload.id)),
      };
    }
    default:
      return state;
  }
};