With Redux in place, let's talk about how we actually modify the Redux state from within our applications.
Yesterday we went through the difficult part of integrating our React app with Redux. From here on out, we'll be defining functionality with our Redux setup.
As it stands now, we have our demo application showing the current time. But there currently isn't any way to update to the new time. Let's modify this now.
Triggering updates
Recall that the only way we can change data in Redux is through an action creator. We created a redux store yesterday, but we haven't created a way for us to update the store.
What we want is the ability for our users to update the time by clicking on a button. In order to add this functionality, we'll have to take a few steps:
- Create an actionCreator to dispatch the action on our store
- Call the actionCreator
onClick
of an element - Handle the action in the reducer
We already implemented the third step, so we only have two things to do to get this functionality working as we expect.
Yesterday, we discussed what actions are, but not really why we are using this thing called actionCreators or what they are.
As a refresher, an action is a simple object that must include a type
value. We created a types.js
file that holds on to action type constants, so we can use these values as the type
property.
export const FETCH_NEW_TIME = 'FETCH_NEW_TIME';
export const LOGIN = 'USER_LOGIN';
export const LOGOUT = 'USER_LOGOUT';
As a quick review, our actions can be any object value that has the type
key. We can send data along with our action (conventionally, we'll pass extra data along as the payload
of an action).
{
type: types.FETCH_NEW_TIME,
payload: new Date().toString()
}
Now we need to dispatch this along our store
. One way we could do that is by calling the store.dispatch()
function.
store.dispatch({
type: types.FETCH_NEW_TIME,
payload: new Date().toString()
})
However, this is pretty poor practice. Rather than dispatch the action directly, we'll use a function to return an action... the function will create the action (hence the name: actionCreator). This provides us with a better testing story (easy to test), reusability, documentation, and encapsulation of logic.
Let's create our first actionCreator
in a file called redux/actionCreators.js
. We'll export a function who's entire responsibility is to return an appropriate action to dispatch on our store.
import * as types from './types';
export const fetchNewTime = () => ({
type: types.FETCH_NEW_TIME,
payload: new Date().toString(),
})
Now if we call this function, nothing will happen except an action object is returned. How do we get this action to dispatch on the store?
Recall we used the connect()
function export from react-redux
yesterday? The first argument is called mapStateToProps
, which maps the state to a prop object. The connect()
function accepts a second argument which allows us to map functions to props as well. It gets called with the dispatch
function, so here we can bind the function to call dispatch()
on the store.
Let's see this in action. In our src/views/Home/Home.js
file, let's update our call to connect by providing a second function to use the actionCreator we just created. We'll call this function mapDispatchToProps
.
import { fetchNewTime } from '../../../redux/actionCreators';
// ...
const mapDispatchToProps = dispatch => ({
updateTime: () => dispatch(fetchNewTime())
})
// ...
export default connect(
mapStateToProps,
mapDispatchToProps,
)(Home);
Now the updateTime()
function will be passed in as a prop and will call dispatch()
when we fire the action. Let's update our <Home />
component so the user can press a button to update the time.
const Home = (props) => {
return (
<div className="home">
<h1>Welcome home!</h1>
<p>Current time: {props.currentTime}</p>
<button onClick={props.updateTime}>
Update time
</button>
</div>
);
}
Although this example isn't that exciting, it does showcase the features of redux pretty well. Imagine if the button makes a fetch to get new tweets or we have a socket driving the update to our redux store. This basic example demonstrates the full functionality of redux.
Multi-reducers
As it stands now, we have a single reducer for our application. This works for now as we only have a small amount of simple data and (presumably) only one person working on this app. Just imagine the headache it would be to develop with one gigantic switch statement for every single piece of data in our apps...
Ahhhhhhhhhhhhhh...
Redux to the rescue! Redux has a way for us to split up our redux reducers into multiple reducers, each responsible for only a leaf of the state tree.
We can use the combineReducers()
export from redux
to compose an object of reducer functions. For every action that gets triggered, each of these functions will be called with the corresponding action. Let's see this in action.
Let's say that we (perhaps more realistically) want to keep track of the current user. Let's create a currentUser
redux module in... you guessed it: src/redux/currentUser.js
:
touch src/redux/currentUser.js
We'll export the same four values we exported from the currentTime
module... of course, this time it is specific to the currentUser. We've added a basic structure here for handling a current user:
import * as types from './types'
export const initialState = {
user: {},
loggedIn: false
}
export const reducer = (state = initialState, action) => {
switch (action.type) {
case types.LOGIN:
return {
...state, user: action.payload, loggedIn: true};
case types.LOGOUT:
return {
...state, user: {}, loggedIn: false};
default:
return state;
}
}
Let's update our configureStore()
function to take these branches into account, using the combineReducers
to separate out the two branches
import { createStore, combineReducers } from 'redux';
import { rootReducer, initialState } from './reducers'
import { reducer, initialState as userInitialState } from './currentUser'
export const configureStore = () => {
const store = createStore(
combineReducers({
time: rootReducer,
user: reducer
}), // root reducer
{
time: initialState,
user: userInitialState
}, // our initialState
);
return store;
}
export default configureStore;
Let's also update our Home
component mapStateToProps
function to read it's value from the time
reducer
// ...
const mapStateToProps = state => {
// our redux store has `time` and `user` states
return {
currentTime: state.time.currentTime
};
};
// ...
Now we can create the login()
and logout()
action creators to send along the action on our store.
export const login = (user) => ({
type: types.LOGIN,
payload: user
})
// ...
export const logout = () => ({
type: types.LOGOUT,
})
Now we can use the actionCreators to call login
and logout
just like the updateTime()
action creator.
Phew! This was another hefty day of Redux code. Today, we completed the circle between data updating and storing data in the global Redux state. In addition, we learned how to extend Redux to use multiple reducers and actions as well as multiple connected components.
However, we have yet to make an asynchronous call for off-site data. Tomorrow we'll get into how to use middleware with Redux, which will give us the ability to handle fetching remote data from within our app and still use the power of Redux to keep our data.
Good job today and see you tomorrow!