Search code examples
reactjsreact-reduxreact-hooksthunk

How to transform class-based React-Redux code using Hooks


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

Solution

  • 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