Updating the Store Via Actions

When a user clicks on the name of an author in the Authors component, you want to update the selectedAuthor property in the global store. Maybe the useSelector function returns a setter like useState? If so, you could write something like this:

const [selectedAuthor, setSelectedAuthor] = useSelector(state => state.selectedAuthor);

But useSelector doesn't return a setter. Redux instead expects you to dispatch an action. An action is an object that describes how the state should change. It has two properties:

An action to change the selected author might look like this:

{
type: 'SelectAuthor',
payload: 'Audre Lord'
}

In src/actions.js, you declare an enum-like object defining the various action types that you will need in Quotebase:

export const Action = Object.freeze({
SelectAuthor: 'SelectAuthor',
AddQuotation: 'AddQuotation',
});

JavaScript doesn't currently support enums. A read-only object whose keys serve as the enumerated values is as close as you can get.

In this same file you create a couple of action creators, which are convenience functions that create action objects:

export function selectAuthor(author) {
return {type: Action.SelectAuthor, payload: author};
}

export function addQuotation(author, text) {
return {type: Action.AddQuotation, payload: {author, text}};
}

Redux does not require that you make an actions enum or action creators. These are just common practices.

To fire off an action, you use the dispatch function that Redux provides. There isn't a global function named dispatch. Rather, you query for it using a different function named useDispatch:

import {useSelector, useDispatch} from 'react-redux';

function Authors() {
const dispatch = useDispatch();
// ...
}

The dispatch function needs to be called when an author is clicked on. You add a click event listener that dispatches a SelectAuthor action for the button's author:

// ...
import {selectAuthor} from './actions';

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

return (
<div id="authors-panel" className="panel">
<h3>Authors</h3>
<ul>
{authors.map(author =>
<li key={author}>
<button
className="author-button"
onClick={() => dispatch(selectAuthor(author))}
>{author}</button>
</li>
)}
</ul>
</div>
);
}

You generate the action by calling the action creator, which you must import. The generated action is passed as a parameter to dispatch.

After you dispatch an action, Redux calls the reduce function. It sends in two parameters: the current global state and the action that's meant to change that state. The job of reducer is to synthesize the current state and the action to produce the new state. You alter its definition in store.js to receive both of its parameters:

// ...
import {Action} from './actions';

// ...

function reducer(state, action) {
return state;
}

// ...

You also import the Action enum, because you'll need to be able to detect what kind of action you have received.

It is common for the reducer function to have a switch statement with a case for each action type:

function reducer(state, action) {
switch (action.type) {
case Action.SelectAuthor:
// ...
case Action.AddQuotation:
// ...
default:
return state;
}
}

If the dispatched action changes the state, you must return a new state object. If you instead try to mutate the old state, React won't detect a change and won't re-render any components that depend on the state. You commit the following principle to your brain and fingers: never mutate state.

The spread syntax for cloning objects is useful for producing a new state object. You use it to make a shallow copy of the current state, but then replace any modified key-value pairs:

function reducer(state, action) {
switch (action.type) {
case Action.SelectAuthor:
return {
...state,
selectedAuthor: action.payload,
};
case Action.AddQuotation:
// ...
default:
return state;
}
}

Only the components that depend on selectedAuthor will be re-rendered by this state change. Such a dependency is established when a component calls useSelector and references the the selected author. The Quotations component does exactly this, so it will re-render if the selected author changes.

Try clicking on author's button. Does it fire off an action, trigger the reducer into updating the state, and ultimately cause the Quotations component to change which quotations are rendered?

Handling an AddQuotation action is a little more involved. The quotations object in the state must be updated. If the author is brand new, you want to add a new key-value pair to quotations. If the author already has some quotations stored, then you want to append the new quotation at the end of its current array.

The spread syntax again simplifies the formation of the new state. This time you replace quotations with an object that contains shallow copies of the old quotations but also a new key-value pair for the author and the author's quotations:

function reducer(state, action) {
switch (action.type) {
case Action.SelectAuthor:
// ...
case Action.AddQuotation: {
const {author, text} = action.payload;
const oldQuotations = state.quotations[author] ?? [];
return {
...state,
quotations: {
...state.quotations,
[author]: [...oldQuotations, text],
},
};
}
default:
return state;
}
}

The author key is enclosed in [] so that the value of the variable is used as the key instead of the literal text author.

The new quotations array is built out of the old quotations array, if there was one. The null coalescing operator (??) primes the old quotations array to be empty if the author doesn't have any quotations yet. The oldQuotations assignment could have been written this way:

let oldQuotations;
if (state.quotations.hasOwnProperty(author)) {
oldQuotations = state.quotations[author];
} else {
oldQuotations = [];
}

Null coalescing is less noisy.

You open up the Adder component, import useDispatch and the action creator, and dispatch an AddQuotation action when the user clicks the Add button:

import {useDispatch} from 'react-redux';
import {addQuotation} from './actions';

export function Adder() {
const dispatch = useDispatch();

return (
<div id="adder-panel" className="panel">
{/* ... */}
<button
onClick={() => dispatch(addQuotation(author, text))}
>Add</button>
</div>
);
}

Try adding a new author and quotation to the store. Does the author show up in the authors list? Does clicking on the author make the quotation appear?

Try adding a new quotation for an existing author. Does the quotation automatically appear if the author is already selected?