Reputation: 888
I am struggling to understand how the react-redux and thunks library work.
What I want to achieve is to get all the posts by using some API when accessing a page.
I am making my call to the API in the componentDidMount()
function.
From what I noticed my code gets executed exactly 3 times of which the last one gets the posts.
Here is my postReducer.js
import * as types from "../actions/actionTypes";
import initialState from "../reducers/initialState";
export function postsHaveError(state = false, action) {
switch (action.type) {
case types.LOAD_POSTS_ERROR:
return action.hasError;
default:
return state;
}
}
export function postsAreLoading(state = false, action) {
switch (action.type) {
case types.LOADING_POSTS:
return action.isLoading;
default:
return state;
}
}
export function posts(state = initialState.posts, action) {
switch (action.type) {
case types.LOAD_POSTS_SUCCESS:
return action.posts;
default:
return state;
}
}
// export default rootReducer;
postAction.js
import * as types from "./actionTypes";
import axios from "axios";
export function postsHaveError(bool) {
return {
type: types.LOAD_POSTS_ERROR,
hasError: bool
};
}
export function postsAreLoading(bool) {
return {
type: types.LOADING_POSTS,
isLoading: bool
};
}
export function postsFetchDataSuccess(posts) {
return {
type: types.LOAD_POSTS_SUCCESS,
posts
};
}
export function postsFetchData(url) {
return dispatch => {
dispatch(postsAreLoading(true));
axios
.get(url)
.then(response => {
if (response.status !== 200) {
throw Error(response.statusText);
}
dispatch(postsAreLoading(false));
return response;
})
.then(response => dispatch(postsFetchDataSuccess(response.data)))
.catch(() => dispatch(postsHaveError(true)));
};
}
and the component in which i am trying to get the posts.
import React from "react";
import PostItem from "./PostItem";
import { connect } from "react-redux";
import { postsFetchData } from "../../actions/postActions";
class BlogPage extends React.Component {
constructor(props) {
super(props);
this.state = {
data: null
};
}
componentDidMount() {
this.props.fetchData("http://localhost:3010/api/posts");
}
render() {
if (this.props.hasError) {
return <p>Sorry! There was an error loading the items</p>;
}
if (this.props.isLoading) {
return <p>Loading…</p>;
}
console.log(this.props);
return (
<div>
<div className="grid-style">
<PostItem <<once i have posts they should go here>> />
</div>
</div>
);
}
}
const mapStateToProps = state => {
return {
posts: state.posts,
hasError: state.postsHaveError,
isLoading: state.postsAreLoading
};
};
const mapDispatchToProps = dispatch => {
return {
fetchData: url => dispatch(postsFetchData(url))
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(BlogPage);
index.js
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import registerServiceWorker from "./registerServiceWorker";
import { BrowserRouter } from "react-router-dom";
import configureStore from "./store/configureStore";
import { Provider } from "react-redux";
const store = configureStore();
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>,
document.getElementById("root")
);
registerServiceWorker();
app.js
import React, { Component } from "react";
import "./App.css";
import Header from "./components/common/header.js";
import Footer from "./components/common/footer.js";
import Main from "./components/common/main.js";
import "./layout.scss";
class App extends Component {
render() {
return (
<div className="App">
<Header />
<Main />
<Footer />
</div>
);
}
}
export default App;
and main.js
in which BlogPage resides.
import React from 'react';
import BlogPage from '../blog/BlogPage';
import AboutPage from '../about/AboutPage';
import { Route, Switch } from 'react-router-dom';
import LoginPage from '../authentication/LoginPage';
const Main = () => {
return (
<div>
<section id="one" className="wrapper style2">
<div className="inner">
<Switch>
<Route path="/about" component={AboutPage} />
<Route path="/login" component={LoginPage} />
<Route path="/" component={BlogPage} />
</Switch>
</div>
</section>
</div>
);
};
export default Main;
Upvotes: 1
Views: 1141
Reputation: 19762
Your problem is very similar to this question (I also include a codesandbox to play around with). Please read through it and follow the working example and, most importantly, read the 7 tips (some may not apply to your project; however, I highly recommend you install prop-types
to warn you when you're deviating from a 1:1 redux state).
The problem you're facing is related to this function postsFetchData
not returning the axios
promise (you also have an unnecessary .then()
that has been removed -- this example flows with the example provided below):
actions/blogActions.js
import * as types from '../types';
export const postsFetchData = () => dispatch => {
// dispatch(postsAreLoading(true)); // <== not needed
return axios
.get("http://localhost:3010/api/posts") // API url should be declared here
.then(({ data }) => { // es6 destructuring, data = response.data
/* if (response.status !== 200) {
throw Error(response.statusText);
} */ // <== not needed, the .catch should catch this
// dispatch(postsAreLoading(false)); // <== not needed
// dispatch(postsFetchDataSuccess(response.data)) // <== not needed, just return type and payload
dispatch({ type: types.LOAD_POSTS_SUCCESS, payload: data })
})
.catch(err => dispatch({ type: types.LOAD_POSTS_ERROR, payload: err.toString() }));
}
As mentioned in the linked question, you don't need an isLoading
with Redux connected container-components. Since the props are coming from redux
's store, React will see the prop change and update the connected component accordingly. Instead, you can either use local React state OR simply just check to see if the data is present.
The example below checks if data is present, otherwise it's loading...
BlogPage.js
import isEmpty from "lodash/isEmpty";
import React, { PureComponent } from "react";
import { connect } from "react-redux";
import PostItem from "./PostItem";
import { postsFetchData } from "../../actions/blogActions";
class BlogPage extends PureComponent {
componentDidMount => () => this.props.postsFetchData(); // the API url should be placed in action creator, not here, especially if it's static
render = () => (
this.props.hasError // if this was an error...
? <p>Sorry! There was an error loading the items: {this.props.hasError}</p> // then an show error
: isEmpty(this.props.posts) // otherwise, use lodash's isEmpty to determine if the posts array exists AND has a length of 0, if it does...
? <p>Loading…</p> // then show loading...
: <div className="grid-style"> // otherwise, if there's no error, and there are posts in the posts array...
<PostItem posts={this.props.posts} /> // then show PostItem
</div>
)
}
export default connect(state => ({
// this is just inline mapStateToProps
posts: state.blog.posts
hasError: state.blog.hasError
}),
{ postsFetchData } // this is just an inline mapDispatchToProps
)(BlogPage);
reducers/index.js
import { combineReducers } from 'redux';
import * as types from '../types';
const initialState = {
posts: [], // posts is declared as an array and should stay that way
hasError: '' // hasError is declared as string and should stay that way
}
const blogPostsReducer = (state = initialState, { type, payload }) => {
switch (type) {
case types.LOAD_POSTS_SUCCESS:
return { ...state, posts: payload, hasError: '' }; // spread out any state, then update posts with response data and clear hasError
case types.LOAD_POSTS_ERROR:
return { ...state, hasError: payload }; // spread out any state, and update hasError with the response error
default:
return state;
}
}
export default combineReducers({
blog: blogPostReducer
// include any other reducers here
})
BlogPage.js (with isLoading
local React state)
import isEqual from "lodash/isEqual";
import isEmpty from "lodash/isEmpty";
import React, { Component } from "react";
import { connect } from "react-redux";
import PostItem from "./PostItem";
import { postsFetchData } from "../../actions/blogActions";
class BlogPage extends Component {
state = { isLoading: true };
componentDidUpdate = (prevProps) => { // triggers when props have been updated
const { posts } = this.props; // current posts
const prevPosts = prevProps.posts; // previous posts
const { hasError } = this.props; // current error
const prevError = prevProps.hasError // previous error
if (!isEqual(posts,prevPosts) || hasError !== prevError) { // if the current posts array is not equal to the previous posts array or current error is not equal to previous error...
this.setState({ isLoading: false }); // turn off loading
}
}
componentDidMount => () => this.props.postsFetchData(); // fetch data
render = () => (
this.state.isLoading // if isLoading is true...
? <p>Loading…</p> // then show loading...
: this.props.hasError // otherwise, if there was an error...
? <p>Sorry! There was an error loading the items: {this.props.hasError}</p> // then an show error
: <div className="grid-style"> // otherwise, if isLoading is false and there's no error, then show PostItem
<PostItem posts={this.props.posts} />
</div>
)
}
export default connect(state => ({
// this is just inline mapStateToProps
posts: state.blog.posts
hasError: state.blog.hasError
}),
{ postsFetchData } // this is just an inline mapDispatchToProps
)(BlogPage);
Upvotes: 2