1

I am learning React/Redux and I am trying to refactor this code from class-based to functional/hooks-based code. The application is an exercise I am working on, it has three components Posts.js where I fetch a list of posts from typicode.com. Each post from the fetched list has a button attacked. On onClick, it should show details for each post (PostDetails.js and Comments.js):

Concept

At the moment, both Posts and Comments are class-based components. I need to:

Step 1: Change them to be functional components and use React Hooks but still keep connect(), mapStateToProps and mapDispatchToProps;

Step 2: Implement React-Redux hooks (UseSelector, useDispatch)

App.js

//imports...
const App = () => {
    return (
        <div className="container">
            <div><Posts /></div>
            <div><PostDetails /></div>
        </div>
    )
}

export default App;

actions

import jsonPlaceholder from '../apis/jsonPlaceholder';

export const fetchPosts = () => async dispatch => {
    const response = await jsonPlaceholder.get('/posts');
    dispatch({type: 'FETCH_POSTS', payload: response.data})
};


export const selectPost = post => {
    return ({
        type: 'POST_SELECTED',
        payload: post
    })
}


export const fetchComments = (id) => async dispatch => {
    const response = await jsonPlaceholder.get(`/comments?postId=${id}`);
    dispatch({type: 'FETCH_COMMENTS', payload: response.data})
}

reducers

export default (state = [], action) => {
    switch (action.type) {
        case 'FETCH_POSTS':
            return action.payload;
        default:
            return state;
    }
}

export default (selectedPost = null, action) => {
    if (action.type === 'POST_SELECTED') {
        return action.payload;
    }
    return selectedPost;
}

export default (state = [], action) => {
    switch (action.type) {
        case 'FETCH_COMMENTS':
            return action.payload;
        default:
            return state;
    }
}

export default combineReducers({
    posts: postsReducer,
    selectedPost: selectedPostReducer,
    comments: commentsReducer
})

components/Posts.js

import React from 'react';
import { connect } from 'react-redux';
import { fetchPosts, selectPost } from '../actions';
import '../styles/posts.scss';


class Posts extends React.Component {
    componentDidMount() {
        this.props.fetchPosts()
    }

    renderPosts() {
        return this.props.posts.map(post => {
            if (post.id <= 10)              
            return (
                <div className='item' key={post.id}>
                    <div className="title">
                        <h4>{post.title}</h4>
                    </div>
                    <button
                        onClick={() => {
                            this.props.selectPost(post)
                            console.log(post)
                        }
                    }>Open</button>
                    <hr/>
                </div>
                )
         })
    }

    render() {
        return(
            <div className="list">
                { this.renderPosts() }
            </div>
        )
  }
    
}

const mapStateToProps = state => {
    return {
        posts: state.posts,
        selectedPost: state.post
    }
};

const mapDispatchToProps = {
    fetchPosts,
    selectPost
}

export default connect(mapStateToProps, mapDispatchToProps)(Posts);

components/PostDetails.js

import React from 'react';
import { connect } from 'react-redux';
import Comments from './Comments'

const PostDetails = ({ post }) => {
    if (!post) {
        return <div>Select a post</div>
    }
    return (
        <div className="post-details">
            <div className="post-content">
                <h3>{post.title}</h3>
                <p>{post.body}</p>
                <hr/>
            </div>
            <div className="comments-detail">
                <Comments postId={post.id}/>
            </div>
        </div>
    )
}

const mapStateToProps = state => {
    return {post: state.selectedPost}
}


export default connect(mapStateToProps)(PostDetails);

components/Comments.js

import React from 'react';
import { connect } from 'react-redux';
import { fetchComments } from '../actions'

class Comments extends React.Component {
    componentDidUpdate(prevProps) {
        if (this.props.postId && this.props.postId !== prevProps.postId){
            this.props.fetchComments(this.props.postId)
        }
    }

    renderComments() {
        console.log(this.props.comments)
        return this.props.comments.map(comment => {
            return (
                <div className="comment" key={comment.id}>
                    <div className="content">
                        <h5>{comment.name}</h5>
                        <p>{comment.body}</p>
                    </div>
                    <hr />
                </div>
            )
        })

    }

    render() {
        return (
            <div className="comments">
                {this.renderComments()}
            </div>
        )
    }
}

const mapStateToProps = state => {
    return {comments: state.comments}
}

export default connect(mapStateToProps, {fetchComments})(Comments);
skyboyer
  • 22,209
  • 7
  • 57
  • 64
Monika
  • 87
  • 1
  • 7
  • If you are converting this into functional components and hooks, you can use `useSelector` and `useDispatch` hooks and get rid of `connect`. Also, you can manage your component state using `useState` hook. – Ajeet Shah Feb 22 '21 at 18:43
  • I know, but I am not sure how to implement them. I have a conflict between using Redux and useState to manage the state. @AjeetShah – Monika Feb 22 '21 at 19:02

1 Answers1

1

This could be a way to create Posts component:

I am assuming that when you dispatch fetchPosts() action, you are saving its response using reducers in Redux.

And, you don't need fetchedPosts in local component state as you already have this data in your Redux state.

const Posts = () => {
  const posts = useSelector((state) => state.posts)
  const dispatch = useDispatch()
  // const [fetchedPosts, setFetchedPosts] = useState([]) // NOT needed

  useEffect(() => {
    dispatch(fetchPosts())
    // setFetchedPosts(posts) // NOT needed
    // console.log(posts) // NOT needed, its value may confuse you
  }, [])

  // Do this, if you want to see `posts` in browser log
  useEffect(() => {
    console.log(posts)
  }, [posts])

  /* NOT needed
  const renderPosts = () => {
    posts.map((post) => {
      console.log(post)
    })
  } */

  return (
    <>
      {posts.map((post) => (
        <div key={post.id}>{post.title}</div>
      ))}
    </>
  )
}

export default Posts
Ajeet Shah
  • 18,551
  • 8
  • 57
  • 87
  • Uhhh, great, this solved the infinite loop problem I had with useEffect. I also assumed that setState would not be needed, however, I was not exactly sure, so thanks for clearing that up! I am moving on to solving the next component! Thanks a lot! – Monika Feb 22 '21 at 20:13
  • I don't understand Ajeet. I am new to StackOverflow. Do I have to do something to give you a vote for helping me? It will be a pleasure mate :) Just let me know how – Monika Feb 22 '21 at 20:18