redux integration - wbobeirne/nycda-ecommerce-server GitHub Wiki
Now that you've seen how the API works and how to build a fetch client you'll want to use Redux to power your React app's data. This is a brief overview of all the things we'll cover in class.
First we'll need to use the react-redux module to help out here. Much like react-router-dom, react-redux provides is with some functionality to enhance our components with additional props. We'll start off by adding the <Provider> component at the top of our App.jsx component, like the <BrowserRouter>:
// src/App.jsx
import { Provider } from "react-redux";
import { createStore } from "redux";
import reducers from "./reducers";
const store = createStore(reducers);
class App extends React.Component {
render() {
return (
<Provider store={store}>
<BrowserRouter>
{/* ... */}
</BrowserRouter>
</Provider>
);
}
}This wrapper component causes our component tree to re-render when the store changes. It also allows us to use the second and last piece of functionality react-redux provides, the connect(mapStateToProps, [mapDispatchToProps]) function, which generates a wrapper function for our component:
// src/pages/Page.jsx
import { connect } from "redux-thunk";
class Page extends React.Component {
/* ... */
}
Page.propTypes = {
// Redux State
user: PropTypes.shape(/* ... */),
userSettings: PropTypes.shape(/* ... */),
messages: PropTypes.arrayOf(/* ... */),
};
function mapStateToProps(state, props) {
return {
user: state.user.user,
userSettings: state.user.settings,
messages: state.messages.messages,
};
}
export default connect(mapStateToProps)(Page);It takes in some configurations, and returns a function that we wrap our component in that adds props functionality to it. This is what's known as a "Higher Order Component", or HOC. React router does the same thing using the <Route> component, this is just a different syntax for the same concept.
We can also map action creators to become prop functions on the component as well. This is especially handy since we don't have to dispatch, it will map dispatch to the action creator functions, so that you don't have to do store.dispatch(actionCreator()), just this.props.actionCreator(). We do so by either passing it a function like mapStateToProps that gets the dispatch function, and returns an object of props, or even shorter, we can just pass it an object of action creators and have it do that for us.
import { sendMessage } from "actions/messages";
import { logout } from "actions/users";
// Component...
Page.propTypes = {
// Redux State
/* ... */
// Action Dispatchers
sendMessage: PropTypes.func.isRequired,
logout: PropTypes.func.isRequired,
};
export default connect(mapStateToProps, {
sendMessage,
logout,
})(Page);Now this component can call this.props.sendMessage("Message", this.props.user) and be updated as soon as the message send action is finished running.
There are more configurations for connect(), but these are the only two you'll really need.
You can find examples of these hookups in the example project.
- Provider component in App.jsx
- Connect in
pages/Search.jsxandpages/Gif.jsx
We've talked about using Redux action creators for doing things like sending messages, logging users in, fetching data etc. but there's a big problem: Action creators need to by synchronous by default. You call it, it returns an action object, that's it. Fortunately, Redux is aware of this shortcoming, and provides the ability to add "middleware" to make smarter action creators. It will enhance what we're allowed to return from action creators so that we're not just limited to objects.
We'll be using redux-thunk, this middleware allows us to return a function from our action creator, instead of just an object. This function gets two arguments, dispatch and getState. In order to do that, we'll have to add the middleware when we create the store:
// App.jsx
import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import reducers from "./reducers";
const store = createStore(reducers, applyMiddleware(thunk));And now we can return thunks instead of objects! Let's try some out, shall we?
Because we have access to the dispatch function, we can call it as soon or as late as we want.
// actions/coffee.js
export function brewCoffee() {
return function(dispatch) {
setTimeout(() => {
dispatch({ type: "COFFEE_BREW_COMPLETE" });
}, 5000);
}
}Because we have access to the dispatch function, we can dispatch as many actions as we want!
// actions/messages.js
export function showMessage(message, timeout = 3000) {
return function(dispatch) {
// Immediately dispatch the message
dispatch({
type: "MESSAGE_SHOW",
message,
});
// After some time, hide it again
setTimeout(() => {
dispatch({ type: "MESSAGE_HIDE" });
}, timeout);
}
}Here we are dispatching actions based on an asynchronous AJAX call to the API. Immediately we fire an action so that the reducer might indicate that we're loading, so that components could display loaders. When it returns, if it comes back with data, we'll dispatch an action to indicate we've loaded the user, along with the user's data. If anything goes wrong (Logic error or network error) then we'll pass the error message along to the store for display.
// actions/user.js
import API from "util/api";
export function fetchUser(userid) {
return function(dispatch) {
dispatch({ type: "USER_FETCH" });
API.get(`/users/${userid}`).then((res) => {
if (res.data) {
dispatch({
type: "USER_FETCH_SUCCESS",
user: res.data.user
});
}
else {
dispatch({
type: "USER_FETCH_FAILURE",
error: res.error.message
});
}).catch((err) => {
dispatch({
type: "USER_FETCH_FAILURE",
error: "Encountered an error while fetching the user"
});
});
}
}You can also find examples in the example project's searchGifs action and the loadGif action
We aren't required to dispatch anything from a thunk action. If there's some reason you wouldn't want to, you can choose not to.
// actions/numbers.js
export function addOdd(number) {
return function(dispatch) {
if (number % 2 === 1) {
dispatch({ type: "ADD_ODD", number });
}
}
}The above example is kind of contrived, but combined with getState, there are plenty of reasons why you might want to conditionally fire an action!
// actions/bank.js
export function withdraw(accountId, amount) {
return function(dispatch, getState) {
const state = getState();
// If their account exists...
if (state.bank.accounts[accountId]) {
// And they have enough money...
if (state.bank.accounts[accountId].balance > amount) {
dispatch({ type: "BANK_WITHDRAW", amount });
}
else {
dispatch({
type: "BANK_ERROR",
error: "Not enough funds for that withdrawal",
});
}
}
else {
dispatch({
type: "BANK_ERROR",
error: "Invalid account ID",
});
}
}
}You can save yourself some time by caching data and serving that instead of firing network requests. In your reducer, save the data from the network with an easy way to look it up. Then when you do the request a second time, dispatch the success immediately!
export function getArticle(articleId) {
return function(dispatch, getState) {
const state = getState();
// If we have it, serve it up immediately
if (state.articlesLookup[articleId]) {
return dispatch({
type: "ARTICLE_FETCH_SUCCESS",
article: state.articlesLookup[articleId],
});
}
// Otherwise, go get it, and save it in the reducer success case
dispatch({ type: "ARTICLE_FETCH" });
API.get(`/articles/${articleId}`).then((res) => {
if (res.data) {
return dispatch({
type: "ARTICLE_FETCH_SUCCESS",
article: res.data.article,
});
}
else {
return dispatch({
type: "ARTICLE_FETCH_FAILURE",
error: res.error.message,
});
}
}).catch((err) => {
return dispatch({
type: "ARTICLE_FETCH_FAILURE",
error: "An error occured while fetching the article",
});
});
}
}An example of this can also be found in the example project's loadGif action.
Sometimes you want to keep firing something until a condition happens, or forever. With a combination of asynchronous calls, timeouts, and getState, we can keep dispatching until something happens. Alternatively, you could just dispatch forever.
export function startTimer() {
return function(dispatch, getState) {
// Make sure we don't have two timers running
const state = getState();
if (state.isTimerRunning) {
return;
}
setInterval(() => {
const freshState = getState();
if (!freshState.isPaused) {
dispatch({ type: "TIMER_INCREMENT" });
}
}, 1000);
}
}
export function pauseTimer() {
// sets isPaused = true in reducer
return { type: "TIMER_PAUSE" };
}
export function resumeTimer() {
// sets isPaused = false in reducer
return { type: "TIMER_RESUME" };
}