React Redux for Beginners(part_8)

part1 & part2 & part3 & part4 & part5 & part6 & part7

Sanje Qi
7 min readNov 3, 2019

Actions

First, let’s define some actions.

Actions are payloads of information that send data from your application to your store. They are the only source of information for the store. You send them to the store using store.dispatch().

Here’s an example action which represents adding a new todo item:

const ADD_TODO = 'ADD_TODO'{
type: ADD_TODO,
text: 'Build my first Redux app'
}

Actions are plain JavaScript objects. Actions must have a type property that indicates the type of action being performed. Types should typically be defined as string constants. Once your app is large enough, you may want to move them into a separate module.

import { ADD_TODO, REMOVE_TODO } from '../actionTypes'

Note on Boilerplate

You don’t have to define action type constants in a separate file, or even to define them at all. For a small project, it might be easier to just use string literals for action types. However, there are some benefits to explicitly declaring constants in larger codebases. Read Reducing Boilerplate for more practical tips on keeping your codebase clean.

Other than type, the structure of an action object is really up to you. If you're interested, check out Flux Standard Action for recommendations on how actions could be constructed.

We’ll add one more action type to describe a user ticking off a todo as completed. We refer to a particular todo by index because we store them in an array. In a real app, it is wiser to generate a unique ID every time something new is created.

{
type: TOGGLE_TODO,
index: 5
}

It’s a good idea to pass as little data in each action as possible. For example, it’s better to pass index than the whole todo object.

Finally, we’ll add one more action type for changing the currently visible todos.

{
type: SET_VISIBILITY_FILTER,
filter: SHOW_COMPLETED
}

REDUX: HOW TO COMBINE REDUCERS

You have used the Redux store and a reducer to define an initial state of stories and to retrieve this state for your component tree. But there is no state manipulation happening yet. In the following sections, you are going to implement the archiving a story feature. When approaching this feature, the simplest thing to do would be to remove the story to be archived from the list of stories in the state in the storyReducer. But let's approach this from a different angle to have a greater impact in the long run. It could be useful to have all stories in the state in the end, but have a way to distinguish between them: stories and archived stories. Following this way, you may be able in the future to have a second React component that shows the archived stories next to the available stories.

From an implementation point of view, the storyReducer will stay as it is for now. But you can introduce a second reducer in a src/reducers/archive.js file, a archiveReducer, that keeps a list of references to the archived stories.

const INITIAL_STATE = [];function archiveReducer(state = INITIAL_STATE, action) {switch(action.type) {default : return state;}}export default archiveReducer;

You will implement the action to archive a story in a moment.

First, the Redux store in its instantiation in the src/store/index.js file needs to get both reducers instead of only the storyReducer. Since the Redux store takes only one reducer, you have to combine both of your reducers to one reducer somehow. Let's pretend that the store can import the combined reducer from the entry file for the reducers, the reducers/index.js file, without worrying about combining the reducers.

import { createStore } from 'redux';import rootReducer from '../reducers';const store = createStore(rootReducer);export default store;

Next you can combine both reducers in the src/reducers/index.js file with Redux’s helper function combineReducers(). Then the combined root reducer can be used by the Redux store.

import { combineReducers } from 'redux';import storyReducer from './story';import archiveReducer from './archive';const rootReducer = combineReducers({storyState: storyReducer,archiveState: archiveReducer,});export default rootReducer;

Since your state is sliced up into two substates now, you have to adjust how you retrieve the stories from your store in the src/index.js file with the intermediate storyState now. This is a crucial step, because it shows how combined reducers slice up your state into substates.

ReactDOM.render(<Appstories={store.getState().storyState}onArchive={() => {}}/>,document.getElementById('root'));

The application should show up the same stories as before when you start it. You can find this section of the tutorial in the GitHub repository. However, there is still no state manipulation happening, because no actions are involved yet. Finally in the next part you will dispatch your first action to archive a story.

REDUX: ACTIONS

In this section, you will dispatch your first action to archive a story. The archive action needs to be captured in the new archiveReducer in the src/reducers/archive.js. file. It simply stores all archived stories by their id in a list. There is no need to duplicate the story. The initial state is an empty list, because no story is archived in the beginning. When archiving a story, all the previous ids in the state and the new archived id will be merged in a new array. The JavaScript spread operator is used here.

import { STORY_ARCHIVE } from '../constants/actionTypes';const INITIAL_STATE = [];const applyArchiveStory = (state, action) =>[ ...state, action.id ];function archiveReducer(state = INITIAL_STATE, action) {switch(action.type) {case STORY_ARCHIVE : {return applyArchiveStory(state, action);}default : return state;}}export default archiveReducer;

The action type is already outsourced in another src/constants/actionTypes.js file. This way it can be reused when dispatching the action from the Redux store or when acting on the action type in another reducer.

export const STORY_ARCHIVE = 'STORY_ARCHIVE';

Last but not least, you can import the action type and use it to dispatch the action in your React entry point where you had the empty function before. Now the passed onArchive() function will dispatch an action when it is used.

import React from 'react';import ReactDOM from 'react-dom';import App from './components/App';import store from './store';import { STORY_ARCHIVE } from './constants/actionTypes';import './index.css';ReactDOM.render(<Appstories={store.getState().storyState}onArchive={id => store.dispatch({ type: STORY_ARCHIVE, id })}/>,document.getElementById('root'));

You can check again your Story component which uses the action when clicking the button. The click on the button triggers the passed function and passes the id of the story.

When you start your application, it should still work, but nothing happens yet when you archive a story. That’s because the archived stories are not evaluated yet. The stories prop that is passed from the src/index.js file to the App component still uses all the stories from the storyState.

Action Creators

Action creators are exactly that — functions that create actions. It’s easy to conflate the terms “action” and “action creator”, so do your best to use the proper term.

In Redux, action creators simply return an action:

function addTodo(text) {
return {
type: ADD_TODO,
text
}
}

This makes them portable and easy to test.

In traditional Flux, action creators often trigger a dispatch when invoked, like so:

function addTodoWithDispatch(text) {
const action = {
type: ADD_TODO,
text
}
dispatch(action)
}

In Redux this is not the case.
Instead, to actually initiate a dispatch, pass the result to the dispatch() function:

dispatch(addTodo(text))
dispatch(completeTodo(index))

Alternatively, you can create a bound action creator that automatically dispatches:

const boundAddTodo = text => dispatch(addTodo(text))
const boundCompleteTodo = index => dispatch(completeTodo(index))

Now you’ll be able to call them directly:

boundAddTodo(text)
boundCompleteTodo(index)

The dispatch() function can be accessed directly from the store as store.dispatch(), but more likely you'll access it using a helper like react-redux's connect(). You can use bindActionCreators() to automatically bind many action creators to a dispatch() function.

Action creators can also be asynchronous and have side-effects. You can read about async actions in the advanced tutorial to learn how to handle AJAX responses and compose action creators into async control flow. Don’t skip ahead to async actions until you’ve completed the basics tutorial, as it covers other important concepts that are prerequisite for the advanced tutorial and async actions.

REDUX: SELECTORS

You can use both substates now, storyState and archiveState, to derive the list of stories that are not archived. The deriving of those properties from the state can either happen directly when passing props from the Redux store to the components or in an intermediate layer which can be called Redux selectors.

You can create your first selector in a new src/selectors/story.js file that only returns the part of the stories that is not archived. The archiveState is the list of archived ids.

const isNotArchived = archivedIds => story =>archivedIds.indexOf(story.objectID) === -1;const getReadableStories = ({ storyState, archiveState }) =>storyState.filter(isNotArchived(archiveState));export {getReadableStories,};

The selector makes heavily use of JavaScript ES6 arrow functions, JavaScript ES6 destructuring and a higher-order function: isNotArchived(). If you are not used to JavaScript ES6, don't feel intimidated by it. It is only a way to express these functions more concise. In plain JavaScript ES5 it would look like the following:

function isNotArchived(archivedIds) {return function (story) {return archivedIds.indexOf(story.objectID) === -1;};}function getReadableStories(state) {return state.storyState.filter(isNotArchived(state.archiveState));}export {getReadableStories,};

Last but not least, you can use the selector to compute the not archived stories instead of retrieving the whole list of stories from the store directly in your src/index.js file.

import React from 'react';import ReactDOM from 'react-dom';import App from './components/App';import store from './store';import { getReadableStories } from './selectors/story';import { STORY_ARCHIVE } from './constants/actionTypes';import './index.css';ReactDOM.render(<Appstories={getReadableStories(store.getState())}onArchive={id => store.dispatch({ type: STORY_ARCHIVE, id })}/>,document.getElementById('root'));

Keep in mind that selectors are not mandatory in Redux. You could have defined the function to retrieve all readable stories from the Redux store in the src/index.js file without ever calling it a selector. It is just a way to retrieve derived state from your Redux store.

You can find this section of the tutorial in the GitHub repository. When you start your application, still nothing happens when you archive a story. Even though you are using the readable stories instead of all stories now. That’s because there is no re-rendering of the React View in place to update it when something in the Redux store has changed. To be continued…

Source: StackOverflow, Robin Wieruch, Medium, ReactJs, MDN, bitsrc

--

--