1

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;
2
  • Are you missing a function in your reducer? Postsloadingsuccess? Commented Oct 10, 2018 at 22:28
  • Why do you have 3 reducers? You should consolidate it into one reducer. Also, are you getting results? If not, maybe your redux-thunk wasn't properly configured. Commented Oct 10, 2018 at 23:24

1 Answer 1

2

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);
Sign up to request clarification or add additional context in comments.

5 Comments

That's a good sum-up. As mentioned in the linked question, you don't need an isLoading with Redux connected container-components - it's impossible to hook up loading indicator for this example otherwise, isn't it? In IRL, this likely would be done via Axios interceptor.
Impossible, no. Just use local react state. I'll update my answer to include another version where state is used.
Updated answer to include an isLoading example.
Nice one. I see no problems managing it with Redux, but local state won't hurt for this task.
As presented, the BlogPage component can be managed entirely by React local state. Redux is not needed. I'm half-tempted to rewrite my answer using just local state...

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.