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
Related
I'm learning redux hooks from library "react-redux" because I need to apply Redux also in the functional components of my project.
So far I don't understand how can be used the same project structure of the redux HOC with connect that I use for the class components.
Specifically I have a separate action file which invoke my API with axios:
FoosActions.js
import axios from "axios";
import {
GET_FOO,
} from "./Types";
};
export const getFoo = () => async (dispatch) => {
const res = await axios.get("/api/v1/foos");
dispatch({
type: GET_FOO,
payload: res.data,
});
};
FooList.js:
import { connect } from "react-redux";
import { getFoos } from "../../actions/FoosActions";
class FoosList extends Component {
constructor() {
super();
this.state = {
errors: {},
};
}
componentDidMount() {
this.props.getFoos();
}
render() {
const { data } = this.props.foo;
return (
<div className="container">
<h2>foo data fetched from API endpoint : </h2>
<ul>
{data.map((foo) => {
return (
<li>
{foo.id} - {foo.name}
</li>
);
})}
<ul>
</div>
</div>
</div>
);
}
}
const mapStateToProps = (state) => ({
foo: state.foo,
errors: state.errors,
});
export default connect(mapStateToProps, { getFoos })(FooList);
FooReducer,js
import { GET_FOO} from "../actions/Types";
const initialState = {
foos: [],
};
export default function (state = initialState, action) {
switch (action.type) {
case GET_FOO:
return {
...state,
foos: action.payload,
};
Now instead in my Functional Component:
FooListFC.js
import { useDispatch, useSelector } from "react-redux";
import { getFoo } from "../../actions/FoosActions";
const Mapping = (props) => {
const [foo, setFoo] = useState([]);
const dispatch = useDispatch();
useEffect(() => {
dispatch(getFoo());
const fooRetrieved = useSelector((state) => state.foo);
setFoo(fooRetrieved);
}, []);
return (
<div className="container">
<h2>foo data fetched from API endpoint : </h2>
<ul>
{foo.map((foo) => {
return (
<li>
{foo.id} - {foo.name}
</li>
);
})}
</ul>
</div>
)
}
How can I reproduce the same behavior of fetching data from API in class component with actions in a different file and using redux hooks (my code in the functional component is not working) ?
Is it a bad practice having both approaches in the same project?
you are able to reproduce the same behaviour, in the function component you can use the selector only instead of both useSelector and useState:
const Mapping = (props) => {
const foo = useSelector((state) => state.foo);
const dispatch = useDispatch();
useEffect(() => {
dispatch(getFoo());
}, []);
...
currently working on adding the items to cart using react and redux but the add item does not work
I'm taking the items from my collections page and then passing the key to the product preview page
I'm using react-redux cartReducer the three files are
just can't figure out how to pass the fish products
product page
cart actions
cart reducer
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import firebase from '../../firebase/firebase';
import { connect } from 'react-redux';
import { addItem } from '../../redux/cart/cart-actions'
class FishPage extends Component {
constructor(props) {
super(props);
this.ref = firebase.firestore().collection('fishproducts');
this.unsubscribe = null;
this.state = {
fishproducts: []
};
}
componentDidMount() {
const ref = firebase.firestore().collection('fishproducts').doc(this.props.match.params.id);
ref.get().then((doc) => {
if (doc.exists) {
this.setState({
fishproducts: doc.data(),
key: doc.id,
isLoading: false
});
} else {
console.log("No such document!");
}
});
}
render() {
return (
<div >
<div>
<div>
<h4><Link to="/">back</Link></h4>
<h3>
{this.state.fishproducts.name}
</h3>
</div>
<div >
<dl>
<dt>Description:</dt>
<dd>{this.state.fishproducts.description}</dd>
<dt>Discount:</dt>
<dd>{this.state.fishproducts.discount}</dd>
<dt>Size:</dt>
<dd>{this.state.fishproducts.size}</dd>
<dt>Weight:</dt>
<dd>{this.state.fishproducts.weight}</dd>
<dt>Price:</dt>
<dd>{this.state.fishproducts.price}</dd>
<dt>Stock:</dt>
<dd>{this.state.fishproducts.stock}</dd>
</dl>
<button onClick={() => addItem(this.state.fishproducts)} >ADD TO CART</button>
</div>
</div>
</div>
);
}
}
const mapDispatchToProps = dispatch => ({
addItem: item => dispatch(addItem(item))
})
export default connect(null, mapDispatchToProps)(FishPage);```
this is cart action page
```import CartActionTypes from './cart-types';
export const toggleCartHidden = () => ({
type:CartActionTypes.TOGGLE_CART_HIDDEN
});
export const addItem = item => ({
type: CartActionTypes.ADD_ITEM,
payload: item
})```
this is cart reducer
```import CartActionTypes from './cart-types';
const INITIAL_STATE = {
hidden: true,
cartItems: []
};
export const cartReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case CartActionTypes.TOGGLE_CART_HIDDEN:
return {
...state,
hidden: !state.hidden
};
case CartActionTypes.ADD_ITEM:
return {
...state,
//cartItems: addItem(state.cartItems, action.payload)
cartItems: [...state.cartItems,action.payload]
};
default:
return state;
}
}
export default cartReducer;```
cant figure out how to pass fishproducts
So concept of React is that you need to access Firebase with a function. For that you should use a functional component.
React allows Hooks to get access to your state without a constructor so that's all
and then you'll need to use dispatch.
import React, { useState, useEffect } from 'react';
import firebase from '../../firebase/firebase';
import { Link } from 'react-router-dom';
import { connect , useDispatch} from "react-redux";
import { addItem} from '../../redux/cart/cart-actions';
const FishPage = (props) => {
const [state, setState] = useState({
name: '',
… rest of the values
isLoading: true,
})
const { name, … rest of the values } = state;
useEffect(() => {
setState({ isLoading: true });
const ref = firebase.firestore().collection('fishproducts').doc(props.match.params.id);
ref.get().then((doc) => {
setState({
name: doc.data().name,
… rest of the values
isLoading: false,
});
})
}, [props.match.params.id])
const item = [];
const dispatch = useDispatch();
return (
<div >
<div>
//your body here
<button onClick={() => dispatch(addItem(item))} >ADD TO CART</button>
</div>
</div>
</div>
);
}
const mapDispatchToProps = dispatch => {
return{
addItem: (item) => dispatch(addItem(item))
}
}
export default connect(null, mapDispatchToProps)(FishPage)
I have been using Redux for a long time, but now decided to try out the new ContextAPI.
I got it working with one component/page (using NextJs), however the state isn't shared between pages/components.
store.js
import React, { createContext, useReducer } from 'react';
import reducer from './reducer'
const initialState = {
players: [],
};
const Store = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<Context.Provider value={[state, dispatch]}>
{children}
</Context.Provider>
)
};
export const Context = createContext(initialState);
export default Store;
reducer.js
const Reducer = (state, action) => {
switch (action.type) {
case 'ADD_PLAYER':
return {
...state,
players: [...state.players, action.payload],
};
case 'REMOVE_PLAYER_BY_INDEX':
const array = state.players;
if (array) {
array.splice(action.payload, 1);
}
return {
...state,
players: !array ? [] : array,
};
default:
return state;
}
};
export default Reducer;
add players page /players/add (addplayerspage.js)
import React, { useContext } from 'react';
import map from 'lodash/map';
import isEqual from 'lodash/isEqual';
import { Context } from '../../../context';
const PlayerCreatePage = () => {
const [_, dispatch] = useContext(Context);
const handleAddPlayer = () => {
dispatch({ type: 'ADD_PLAYER', payload: Math.random() });
};
const handleRemovePlayerByIndex = (index) => {
dispatch({ type: 'REMOVE_PLAYER_BY_INDEX', payload: index });
};
return (
<div className="layout">
<div>
<Context.Consumer>
{([state]) => {
const { players } = state;
return map(players, (p, i) => <div
key={i}
onClick={() => handleRemovePlayerByIndex(i)}
>
{p}
</div>
)
}}
</Context.Consumer>
</div>
<button onClick={() => handleAddPlayer()}>Add new</button>
</div>
);
};
export default React.memo(PlayerCreatePage, (prev, next) => isEqual(prev, next));
lobby players page /players/lobby (lobbyplayerspage.js)
import React, { useContext } from 'react';
import map from 'lodash/map';
import { Context } from '../../../context';
const PlayersLobbyPage = () => {
const [state, _] = useContext(Context);
return (
<div>
<div>
{map(state.players, (p, i) => <div
key={i}
>
{p}
</div>
)}
</div>
</div>
);
};
export default PlayersLobbyPage;
_app.js (NextJs)
import App, { Container } from 'next/app';
import '../styles/main.css';
import Store from '../context';
class MyApp extends App {
render() {
const { Component, pageProps } = this.props;
return (
<Container>
<Store>
<Component {...pageProps} />
</Store>
</Container>
);
}
}
export default MyApp;
THE PROBLEM:
Have two tabs open
Add players
Lobby
Add a new player
See that player is added on 'Add players' page
2.See that NOTHING happens on 'Lobby' page
Okay, so the issue is that I was trying to "share" context api state between different open tabs, it doesn't work like that by default, even for redux (tried adding it and faced same result), for redux there's a redux-state-sync library for that, nonetheless I will use sockets in future, so this won't be an issue.
Closed.
I have to set a value on from a API into a newly created <button> component handled by Redux, but I don't know if I can use setState for this. I created a reducer and an action SET_VOTE_COUNT but I'm not seeing how this is done. This is my first Redux project, so here is the code:
// ./src/js/components/CounterList.js
import React from 'react';
import { connect } from 'react-redux';
import { increment, decrement } from '../actions/reducer';
import Counter from './Counter';
const CounterList = ({
counters,
onIncrement,
onDecrement
}) => (
<ul>
{counters.map(counter =>
<Counter style={{div: "voting"}}
key={counter.id}
value={counter.count}
onIncrement={() => onIncrement(counter.id)}
onDecrement={() => onDecrement(counter.id)}
/>
)}
</ul>
);
const mapStateToProps = (state) => {
return {
counters: state
};
};
const mapDispatchToProps = (dispatch) => {
return {
onIncrement: (id) => dispatch(increment(id)),
onDecrement: (id) => dispatch(decrement(id))
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(CounterList);
// ./src/js/components/Counter.js
import React, { Component } from 'react';
class Counter extends Component {
render() {
return (
<div className="voting">
<span>{this.props.value}</span>
<button
onClick={() => this.props.onIncrement()}>
+
</button>
<button
onClick={() => this.props.onDecrement()}>
-
</button>
</div>
);
}
}
export default Counter;
import React, {Component} from 'react';
import logo from '../../logo.svg';
import '../../App.css';
import AddButton from './AddButton'
class Posts extends Component {
constructor(props) {
super(props);
this.state = {
response: ''
};
}
componentDidMount() {
fetch(
"/posts"
).then(response => response.json())
.then(data => this.setState({ response: data }))
}
render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
{Array.isArray(this.state.response) &&
this.state.response.map(resIndex => <>
{ resIndex.voteScore}
<AddButton className="voting"/>
<p> { resIndex.title }, by { resIndex.author } </p>
<p> { resIndex.body } </p>
<p> {resIndex.category} </p>
</>
)}
</header>
</div>
)
}
}
export default Posts;
import React from 'react';
import { add_counter, setVoteCount } from '../actions/reducer';
import { connect } from 'react-redux';
const AddButton = ({dispatch}) => (
<div className="voting">
<button
onClick={() => {
dispatch(setVoteCount())
// dispatch(add_counter());
}}>
Vote
</button>
</div>
);
export default connect()(AddButton);
The reducer:
// ./src/js/actions/counters.js
export const setVoteCount = (id) => {
return {
type: "SET_VOTE_COUNT",
id
};
}
export const increment = (id) => {
return {
type: "INCREMENT",
id
};
};
export const decrement = (id) => {
return {
type: "DECREMENT",
id
};
};
export const add_counter = () => {
return {
type: "ADD_COUNTER"
};
};
store action:
import { createStore, applyMiddleware, compose } from 'redux';
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const change_counter = (state = {}, action) => {
switch (action.type) {
case "SET_VOTE_COUNT":
if (state.id !== action.id) {
return state;
}
return {
...state,
count : 37
}
case "INCREMENT":
if (state.id !== action.id) {
return state;
}
return {
...state,
count: state.count+1
};
case "DECREMENT":
if (state.id !== action.id) {
return state;
}
return {
...state,
count: state.count - 1
};
default:
return state;
}
};
let nextId = 0;
const counters = (state = [], action) => {
switch (action.type) {
case "ADD_COUNTER":
return [...state, {id: nextId++, count: 0}];
case "SET_VOTE_COUNT":
return [...state, {id: nextId++, count: action.count}];
case "INCREMENT":
return state.map(counter => change_counter(counter, action));
case "DECREMENT":
return state.map(counter => change_counter(counter, action));
default:
return state;
}
}
export default createStore(counters, composeEnhancers(applyMiddleware()));
I can upload it to GitHub if necessary. Many thanks.
In the AddButton component,the actions should be wrapped in mapDispatchToProps and passed to the connect function. You are calling the raw action in your example, but you need to wrap it with dispatch for it to update the store.
However, I'm not sure what you are trying to update the store with exactly. The action payload is empty in your example, and the reducer has 37 hardcoded as the state.count in response the SET_VOTE_COUNT action type. Did you mean to pass something from the API response?
<AddButton count={resIndex.count} className="voting"/>
import React from 'react';
import { add_counter, setVoteCount } from '../actions/reducer';
import { connect } from 'react-redux';
const mapDispatchToProps = {
setVoteCount
};
const AddButton = props => (
<div className="voting">
<button onClick={() => {
props.setVoteCount(props.count);
}}>
Vote
</button>
</div>
);
export default connect(null, mapDispatchToProps)(AddButton);
I want to list posts in PostList.js component from JSON file
I use react-redux for state managment and redux-saga to get json file
My components are Post.js and PostList.js:
Post.js
const Post = ({ post }) => {
<li>
{post}
</li>
}
export default Post
PostList.js
class PostList extends React.Component {
componentDidMount() {
console.log('did mount');
this.props.fetchPosts();
}
render() {
return (
<div>
<ul>
{this.state.posts(post => (
<Post key={post.id} {...post} />
))}
</ul>
</div>
)
}
}
export default PostList
Reducer
export default (state = [], action) => {
switch (action.type) {
case "FETCH_POSTS":
return {
...state,
loading: true,
posts: []
}
case "FETCH_FAILD":
return {
...state,
loading: false,
posts: []
}
case "FETCH_SUCCESS":
return Object.assign({}, state, {
posts: action.posts
})
default:
return state;
}
}
Actions.js
export const fetchPosts = () => {
return {
type: 'FETCH_POSTS'
}
}
export const fetchSuccess = data => ({
type: "FETCH_SUCCESS",
posts: data
})
export const fetchFaild = () => {
return {
type: 'FETCH_FAILD'
}
}
GetPosts.js (Container)
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import PostList from '../components/PostList'
import { fetchPosts } from '../actions'
const mapStateToProps = state => ({ posts: state.posts });
const mapDispatchToProps = dispatch => bindActionCreators({fetchPosts}, dispatch);
const GetPosts = connect(
mapStateToProps,
mapDispatchToProps
)(PostList)
export default GetPosts
Saga.js
export function* fetchProducts() {
try {
console.log('saga')
const posts = yield call(api_fetchPost);
yield put({ type: "FETCH_SUCCESS", posts});
} catch (e) {
yield put({ type: "FETCH_FAILD", e});
return;
}
}
export function* watchFetchProducts() {
yield takeEvery("FETCH_POSTS", fetchProducts)
}
You are fetching posts from the state of your postlist component. Redux mapStateToProps map the redux state to connected component's props and not state
class PostList extends React.Component {
componentDidMount() {
console.log('did mount');
this.props.fetchPosts();
}
render() {
return (
<div>
<ul>
{this.props.posts && this.props.posts.map(post => {
return ( <Post key={post.id} {...post} /> );
})}
</ul>
</div>
)
}
}
export default PostList
Change this.state.posts to this.props.posts