1
votes

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);
1
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
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

1 Answers

1
votes

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