In step 14 we separated out our "action-reducers" into distinct "actions" and "reducers". As of now, these "actions" and "reducers" are not being consumed by any part of our app. We are still relying on the "action-reducers" from before. Our goal in this step is to actually connect our Redux-y "reducers" and "actions" to our React application by hydrating the data via a Redux store
.
In order to actually connect the two, we will be using some helpers provided by react-redux
, namely: <Provider/>
and connect()
. The docs provide invaluable insight into their behavior, but in short terms:
- Provider: The top level component in your application, makes the
store
accessible via calls toconnect()
for all children components - connect(): A function that provides access to the Redux state and actions via
mapStateToProps
andmapDispatchToProps
.
A component which has been passed to connect takes on a different term: a container. The term comes from an abstraction suggested in redux documentation that components should be split into two basic groups: those which handle presentation and those which handle data. "Containers" are passed to connect()
and so are aware of the state, and are able to dispatch actions, while presentational components simply consume their props and maintain state for UI purposes.
In this step we are going to finish the remaining tasks in order to connect our React application to our Redux state and actions. This will include:
- Creating our
store
inApp.js
- Applying
redux-thunk
as a middleware to handle async actions - Making
<Provider />
the top level component - Refactoring of
<App />
such that its primary job is creating the store, and hydrating the app. The JSX will move into a new container component:<Page />
connect()
<Page />
so that we candispatch()
actions, and access our App's state
As always, if you run into trouble with the tasks or exercises, you can take a peek at the final source code.
If you didn't successfully complete the previous step, you can jump right in by copying the step and installing the dependencies.
Ensure you're in the root folder of the repo:
cd react-workshop
Remove the existing workshop directory if you had previously started elsewhere:
rm -rf workshop
Copy the previous step as a starting point:
cp -r 14-reduxy-action-reducers workshop
Change into the workshop
directory:
cd workshop
Install all of the dependencies (yarn
is preferred):
# Yarn
yarn
# ...or NPM
npm install
Start API server (running at http://localhost:9090/):
# Yarn
yarn run start:api
# ...or NPM
npm run start:api
In a separate terminal window/tab, making sure you're still in the workshop
directory, start the app:
# Yarn
yarn start
# ...or NPM
npm start
After the app is initially built, a new browser window should open up at http://localhost:3000/, and you should be able to continue on with the tasks below.
Create a new folder src/containers/
and within that folder add a new file Page.js
. Copy over the contents of App.js
into the new file, and rename the class to Page
. Also replace all instances of app
in the markup's className
's with page
:
// imports
export default class Page extends PureComponent {
// prop types & default props
// initialize state
// lifecycle and helper methods
render() {
// variable declarations
return(
<main className="page">
<div className="page__page">
<div className="page__list">
{ /* EmailList */ }
{ /* EmailViewWrapper */ }
<button
className="page__new-email"
onClick={this._handleShowForm.bind(this)}
>
+
</button>
{ /* EmailFormWrapper */ }
</div>
</main>
)
}
}
We are going to do the same thing with our App.css
as well. Create a file Page.css
within src/containers
and copy over the contents of App.css
replacing all instances of app
to page
. Then feel free to delete App.css
.
Next we are going to do some ground work to set up the modified app structure. We want the <Page />
to deal with:
- the layout
- handling user interactions
- UI state
- initial hydration of the App
The <App/>
should only concernt itself with setting up the store
.
All <App />
needs to do is render a <Provider />
, and instantiate a store
. To achieve this, <App />
only needs a call to render()
. With this refactor <App />
will become the root of our application, only setting up the store
, <Provider />
, and <Page />
components. Which means consuming pollInterval
and passing it down is not necessary in <App />
either. After removing unnecessary content, the <App/>
should look something like this:
import React, {PureComponent} from 'react';
export default class App extends PureComponent {
render() {
return ()
}
}
Now that we have separated out <App />
into two distinct components, set up <App />
to create a store
and have <Provider />
consume the store as the top level child. Then, we will return to <Page />
and complete it's refactor.
Next, lets create our store
.
The only things we need when creating a store are: createStore()
from redux
, and our root reducer, which in this case is emails
from reducers/index.js
. Additionally, since we are using actions to make API calls which behave asynchronously, we also will need to import applyMiddleware()
from redux
and thunk
from redux-thunk
.
First, import the necessary modules:
import {createStore, applyMiddleware} from 'redux';
import thunk from 'redux-thunk';
import {Provider} from 'react-redux';
import {emails} from './reducers';
export default class App extends PureComponent {
// class methods
}
Next, instantiate the store. We can simply define it as a const
above the class declaration:
// imports
const store = createStore(
emails,
applyMiddleware(thunk)
);
export default class App extends PureComponent {
// class methods
}
We have a store
! Now, that we do, we can modify our render()
function so it returns <Provider />
as the top level component, with <Page />
as its child. Because we are now treating <App />
as the root of our application, pollInterval
should be declared and passed into <Page />
here, and removed from the instantiation in index.js
.
// previously declared imports
import Page from './containers/Page';
// store
export default class App extends PureComponent {
render() {
<Provider store={store}>
<Page pollInterval={5000} />
</Provider>
}
}
After these changes our App.js should look something like:
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {createStore, applyMiddleware} from 'redux';
import thunk from 'redux-thunk';
import {Provider} from 'react-redux';
import Page from './containers/Page';
import {emails} from './reducers';
const store = createStore(
emails,
applyMiddleware(thunk)
);
export default class App extends PureComponent {
render() {
return (
<Provider store={store}>
<Page pollInterval={5000} />
</Provider>
);
}
}
With that, last change our <App />
has been successfully refactored to create our store
.
Lets change focus and start the refactor of <Page />
<Page />
being a "container" means that it will receive the necessary application state and actions via its props
rather than maintaining them itself. <Page />
will still keep track of purely UI data in its state
, such as showForm
and selectedEmailId
. But, any reference to or update of emails
as a property of <Page />
's state will need to be removed, and instead reference props
. But how do they become props
of <Page />
? That is where connect()
comes into play.
But first, lets remove emails
from the state object, and instead add it to <Page/>
's propTypes
and render:
// Page.js
// imports
export default class Page extends PureComponent {
static propTypes = {
pollInterval: PropTypes.number,
emails: PropTypes.arraOf(EMAIL_PROP_TYPE),
}
// default props
state = {
// Initialize selected email ID to -1, indicating nothing is selected.
// When an email is selected in EmailList, this will be updated to
// corresponding ID
selectedEmailId: -1,
// Initialize show form flag to false, indicating that it won't show.
// When the new email button is clicked, it'll be set to `true`. It'll
// be toggled false on form submission or cancel
showForm: false
}
// lifecycle methods
// handlers
// other helpers
render() {
let {emails} = this.props;
let {selectedEmailId, showForm} = this.state;
// returned JSX
}
Now that's out of the way, lets get our component hydrated with connect()
. In general so far, our components have looked like:
export default class Page extends PureComponent {
However, we want to export the "container" or connected component so instead we need to export the connected version. To do that, lets simply declare our class and then at the bottom of Page.js
export default our connected component:
// Page.js
//imports
import {connect} from 'react-redux'
class Page extends PureComponent {
// class methods
}
export default connect()(Page);
Now our default export is a container.
connect()
exposes two functions as its first two arguments:
mapStateToProps
: recievesstate
as a parametermapDispatchToProps
: receivesdispatch
as a parameter
We utilize these to hydrate the component (<Page/>
) passed into connect()
. In order to do so we need to map our application's state to our component's props. So lets modify our default export to look like:
// Page.js
// imports
class Page extends PureComponent {
// class methods
}
export default connect(
//_mapStateToProps
(state) => ({emails: state})
)(Page)
With that, this.props.emails
is coming from our Redux state. Next we are going to address updating our _handle
functions to stop referencing state
and instead utilize actions via dispatch
.
Similarly to emails
now being hydrated via the component's props, our actions shoudld be as well. By passing our actions through mapDispatchToProps
each "action" is wrapped in a call to dispatch()
. Lets import deleteEmail
and pass it through mapDispatchToProps
. Doing this will add deleteEmail()
as a prop to the <Page />
, so lets add it to our propTypes
as well.
// Page.js
// previous imports
import {deleteEmail as deleteEmailAction} from '../actions';
class Page extends PureComponent {
static propTypes = {
pollInterval: PropTypes.number,
emails: PropTypes.arrayOf(EMAIL_PROP_TYPE),
deleteEmail: PropTypes.func,
};
// default props
// initial state
// class methods
// render
}
export default connect(
//_mapStateToProps
(state) => ({emails: state}),
//_mapDispatchToProps
(dispatch) => ({
deleteEmail: () => dispatch(deleteEmailAction)
})
)(Page)
Additionally, we can further optimize this call by taking advantage of a feature of mapDispatchToProps()
. If an object made entirely of action creators is passed directly to _mapDispatchToProps
, it will implicitly wrap each one in a call todispatch()
. This means we can rewrite the above as:
// Page.js
// imports
class Page extends PureComponent {
// class methods
}
export default connect(
//_mapStateToProps
(state) => ({emails: state}),
//_mapDispatchToProps
{
deleteEmail: deleteEmailAction
}
)(Page);
Now we can update our _handleItemDelete()
to utilize the redux action coming through props, rather than our previous version. Addtionally, since we are using our Redux action, and emails
is from our store
we should no longer manually optimistically update our email state upon the action being completed. The store
will handle that all on its own, so instead we can just focus on the UI behavior of resetting the selectedEmail
to -1
. So our _handleItemDelete
should now look something like:
class Page extends PureComponent {
// props
// initial state
// lifecycle methods
_handleItemDelete(emailId) {
this.props.deleteEmail(emailId)
// Also reset `selectedEmailId` since we're deleting it
this.setState({selectedEmailId: -1});
}
// helpers
// render
}
With this, our deleteEmail()
action should properly go through dispatch
to eventually update our store.
As an optional minor optimization, some of the _handlers
's .then()
just update this.state.emails
upon success. If no other side effect occurs in the handler, you can pass the action itself directly to the child component and remove the handler altogether. For example with the markUnread()
action we can delete _handleMarkUnread()
and pass the action directly to the components which consume it, rather than the handler:
class Page extends PureComponent {
// props
// initial state
// lifecycle methods
// delete this handler
// _handleMarkUnread() {
// markUnread();
// }
// helpers
render() {
// var declarations
return (
<main className="page">
<div className="page__page">
<div className="page__list">
<EmailList
// other props
// We can pass markUnread() directly into the handler
onItemMarkUnread={this.props.markUnread}
/>
// components
</div>
</div>
</main>
)
}
}
Within <Page />
there are still many references to our previous version of "action-reducers" and this.state.emails
. However, once these have all been replaced with Redux actions, the app should work just as it did before but now built on the scalability of Redux.
- Replace the remaining "action-reducers":
getEmails()
,addEmail()
,markRead()
,markUnread()
inPage.js
with the actions fromactions/index.js
- Delete action-reducers file (Woo!)
You're done! Your code should mirror the Completed App.
Got questions? Need further clarification? Feel free to post a question in Ben Ilegbodu's AMA!