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