The last major piece of the Redux architecture that's missing from this picture is the action creator functions. These are called by components in order to dispatch payloads to the Redux store. The end result of dispatching any action is a change in state. However, some actions need to go and fetch state before they can be dispatched to the store as a payload.
Let's look at the Home component of the Neckbeard News app. It'll show you how you can pass along action creator functions when wiring up components to the Redux store. Here's the code:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { Map } from 'immutable';
// Various styles...
const listStyle = {
listStyle: 'none',
margin: 0,
padding: 0
};
const listItemStyle = {
margin: '0 5px'
};
const titleStyle = {
background: 'transparent',
border: 'none',
font: 'inherit',
cursor: 'pointer',
padding: '5px 0'
};
// What to render when the article list is empty
// (true/false). When it's empty, a single elipses
// is displayed.
const emptyMap = Map()
.set(true, <li style={listItemStyle}>...</li>)
.set(false, null);
class Home extends Component {
static propTypes = {
articles: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchingArticles: PropTypes.func.isRequired,
fetchArticles: PropTypes.func.isRequired,
toggleArticle: PropTypes.func.isRequired,
filter: PropTypes.string.isRequired
};
static defaultProps = {
filter: ''
};
// When the component is mounted, there's two actions
// to dispatch. First, we want to tell the world that
// we're fetching articles before they're actually
// fetched. Then, we call "fetchArticles()" to perform
// the API call.
componentWillMount() {
this.props.fetchingArticles();
this.props.fetchArticles(this.props.filter);
}
// When an article title is clicked, toggle the state of
// the article by dispatching the toggle article action.
onTitleClick = id => () => this.props.toggleArticle(id);
render() {
const { onTitleClick } = this;
const { articles } = this.props;
return (
<ul style={listStyle}>
{emptyMap.get(articles.length === 0)}
{articles.map(a => (
<li key={a.id} style={listItemStyle}>
<button onClick={onTitleClick(a.id)} style={titleStyle}>
{a.title}
</button>
{/* The summary of the article is displayed
based on the "display" property. This state
is toggled when the user clicks the title. */}
<p style={{ display: a.display }}>
<small>
<span>{a.summary} </span>
<Link to={`articles/${a.id}`}>More...</Link>
</small>
</p>
</li>
))}
</ul>
);
}
}
// The "connect()" function connects this component
// to the Redux store. It accepts two functions as
// arguments...
export default connect(
// Maps the immutable "state" object to a JavaScript
// object. The "ownProps" are plain JSX props that
// are merged into Redux store data.
(state, ownProps) =>
Object.assign(state.get('Home').toJS(), ownProps),
// Sets the action creator functions as props. The
// "dispatch()" function is when actually invokes
// store reducer functions that change the state
// of the store, and cause new prop values to be passed
// to this component.
dispatch => ({
fetchingArticles: () =>
dispatch({
type: 'FETCHING_ARTICLES'
}),
fetchArticles: filter => {
const headers = new Headers();
headers.append('Accept', 'application/json');
fetch(`/api/articles/${filter}`, { headers })
.then(resp => resp.json())
.then(json =>
dispatch({
type: 'FETCH_ARTICLES',
payload: json
})
);
},
toggleArticle: payload =>
dispatch({
type: 'TOGGLE_ARTICLE',
payload
})
})
)(Home);
Let's focus on the connect() function, which is used to connect the Home component to the store. The first argument is a function that takes relevant state from the store and returns it as props for this component. It's using ownProps so that you can pass props directly to the component and override anything from the store. The filter property is why we need this capability.
The second argument is a function that returns action creator functions as props. The dispatch() function is how these action creator functions are able to deliver payloads to the store. For example, the toggleArticle() function is a call directly to dispatch(), and is called in response to the user clicking the article title. However, the fetchingArticles() call involves asynchronous behavior. This means that dispatch() isn't called until the fetch() promise resolves. It's up to you to make sure that nothing unexpected happens in between.
Let's wrap things up by looking at the reducer function used with the Home component:
import { fromJS } from 'immutable';
const typeMap = fromJS({
// Clear any old articles right before
// we fetch new articles.
FETCHING_ARTICLES: state =>
state.update('articles', a => a.clear()),
// Articles have been fetched. Update the
// "articles" state, and make sure that the
// summary display is "none".
FETCH_ARTICLES: (state, payload) =>
state.set(
'articles',
fromJS(payload)
.map(a => a.set('display', 'none'))
),
// Toggles the state of the selected article
// "id". First we have to find the index of
// the article so that we can update it's
// "display" state. If it's already hidden,
// we show it, and vice-versa.
TOGGLE_ARTICLE: (state, id) =>
state.updateIn([
'articles',
state
.get('articles')
.findIndex(a => a.get('id') === id),
'display',
], display =>
display === 'none' ?
'block' : 'none'
),
});
export default (state, { type, payload }) =>
typeMap.get(type, s => s)(state, payload);
The same technique of using a type map to change state based on the action type is used here. Once again, this code is easy to reason about, yet everything that can change in the system is explicit.