Editing Memories

A logical next step is to make the new memory button work. When a new memory is created, you want to immediately show a textarea so that the user can start writing. Since you also want the user to be able to go back and edit old memories, you decide to postpone thinking about adding new memories and concentrate first on being able to edit memories.

Your Memory component currently provides a read-only view. You could create a new MemoryEditor component. However, some of the logic and presentation would be duplicated across the two components. Maybe your Memory component can be used for both viewing and editing? That would mean that the Memory component must provide a different interface depending on whether or not its memory is being edited. The React community calls this conditional rendering.

You don't currently have a way to know a memory's edit status. However, you are the one in charge of your app's state, and you decide to add an isEditing flag to each memory. To ease testing of this editing feature, you comment out the useEffect call in App so that the placeholder memories are displayed. Then you add flags to the two placeholder memories in the store's initial state:

const initialState = {
memories: [
{
id: 1,
year: 2010,
month: 9,
day: 21,
entry: "Today I had a soccer tournament. I was about to score a goal when lightning struck the ball. Now my hair won't settle down.",
isEditing: true,
},
{
id: 2,
year: 2011,
month: 9,
day: 21,
entry: "It's been a year since the lightning incident, and my hair hasn't changed.",
isEditing: false,
},
],
};

The Memory component examines a memory's isEditing flag when deciding what to render. Conditional rendering can take several forms, most which involve writing inelegant JSX. The form you choose is to have two separate return statements:

export function Memory(props) {
// ...

if (memory.isEditing) {
return (
// ...
);
} else {
return (
// ...
);
}
}

When the memory is not being edited, you return the elements you had originally but you add an edit button:

export function Memory(props) {
// ...

if (memory.isEditing) {
return (
// ...
);
} else {
return (
<Fragment>
<div className="memory-left memory-cell">
<span className="year">{memory.year}</span>
<span className="month-day">{monthDay}</span>
<span className="weekday">{weekday}</span>
<button>Edit</button>
</div>
<div className="memory-right memory-cell">{entry}</div>
</Fragment>
);
}
}

When the memory is being edited, you show a similar structure, but you offer a textarea and save, cancel, and delete buttons:

export function Memory(props) {
// ...

if (memory.isEditing) {
return (
<Fragment>
<div className="memory-left memory-cell">
<span className="year">{memory.year}</span>
<span className="month-day">{monthDay}</span>
<span className="weekday">{weekday}</span>
<button>Save</button>
<button>Cancel</button>
<button>Delete</button>
</div>
<div className="memory-right memory-cell">
<textarea />
</div>
</Fragment>
);
} else {
// ...
}
}

The edit button you've added must change a memory's isEditing flag to true. The cancel button must change it back to false. Since the memory is in the global store, you need a couple of actions to go through the reducer and update the memory's state. You add enum instances and action creators for these two actions:

export const Action = Object.freeze({
// ...
StartMemoryEdit: 'StartMemoryEdit',
CancelMemoryEdit: 'CancelMemoryEdit',
});

export function startMemoryEdit(id) {
return {type: Action.StartMemoryEdit, payload: id};
}

export function cancelMemoryEdit(id) {
return {type: Action.CancelMemoryEdit, payload: id};
}

The action creators expect the memory's unique ID to be passed as a parameter. They don't need the whole memory, just a way to figure out which memory in the store needs to have its isEditing flag updated.

In the reducer, you add cases to respond to these two actions. You need to update the memory whose ID matches the action's payload. You'd like to be able to write something like this for canceling an edit:

let memory = state.memories.find(memory => memory.id === action.payload);
memory.isEditing = false;

But mutating state is not the React or Redux way. You need to make your changes to a clone of the memory. A common practice for updating an object buried in an array is to map over the array and selectively replace the target object:

state.memories.map(memory => {
if (memory.id === action.payload) {
return {...memory, isEditing: false};
} else {
return memory;
}
})

This form isn't nearly so compact, but it avoids mutating the state. You apply it for both actions:

function reducer(state, action) {
switch (action.type) {
// ...
case Action.StartMemoryEdit:
return {
...state,
memories: state.memories.map(memory => {
if (memory.id === action.payload) {
return {...memory, isEditing: true};
} else {
return memory;
}
}),
};
case Action.CancelMemoryEdit:
return {
...state,
memories: state.memories.map(memory => {
if (memory.id === action.payload) {
return {...memory, isEditing: false};
} else {
return memory;
}
}),
};
// ...
}
}

Back in the Memory component, you make clicks on the edit and cancel buttons dispatch these actions:

// ...
import {useDispatch} from 'react-redux';
import {startMemoryEdit, cancelMemoryEdit} from './actions';

export function Memory(props) {
// ...
const dispatch = useDispatch();

if (memory.isEditing) {
return (
// ...
<button onClick={() => dispatch(cancelMemoryEdit(memory.id))}>Cancel</button>
// ...
);
} else {
return (
// ...
<button onClick={() => dispatch(startMemoryEdit(memory.id))}>Edit</button>
// ...
);
}
);

Try clicking on the buttons. Do you see the interface toggle back and forth between viewing and editing modes?

The textarea is empty. You want it to show the memory's entry. This brings you to a design decision. Do change events on the textarea immediately update the memory in the global store? Such an approach makes canceling the edits difficult, since you will have overwritten the original entry. A cleaner approach is add local state that initially mirrors the global state. Only when the user commits to the changes by clicking the save button does the local state get pushed to the global state.

You add some local state for the entry, seed it with the text from the global store, and wire up the textarea to read and set this state:

import {Fragment, useState} from 'react';
// ...

export function Memory(props) {
// ...
const [entry, setEntry] = useState(memory.entry);

if (memory.isEditing) {
return (
// ...
<textarea
value={entry}
onChange={event => setEntry(event.target.value)}
// ...
);
} else {
// ...
}
);

Try editing a memory. Does its entry appear in the text area?

You can start editing and you can cancel editing. But what about saving the edits?