I am trying to implement a lazy loading in a MERN stack app like in producthunt. I want to have the posts created on the current date shown by default. If the user scroll down, it will fetch more data on the previous date. I am using react infinite scroll. However, it seems like the app requests to api like an infinite loop without listening on scrolling. I got the following error.
Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
The function is async/await so I don't understand why it keeps calling new requests even though the old request is not resolved yet.
In a Post components
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import Spinner from '../layout/Spinner';
import PostItem from './PostItem';
import UserItem from '../users/UserItem';
import TopDiscussion from '../TopDiscussion';
import SmallAbout from '../SmallAbout';
import { getPostsByDate } from '../../actions/post';
import Moment from 'react-moment';
import InfiniteScroll from 'react-infinite-scroller';
const Posts = ({ getPostsByDate, post: { posts, loading } }) => {
const now = new Date();
const startOfToday = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate()
);
// startOfToday = startOfToday -1
useEffect(() => {
getPostsByDate(startOfToday);
}, [getPostsByDate]);
const [date, setDate] = useState(startOfToday);
const [shown, setShown] = useState();
const getPosts = () => {
getPostsByDate(date);
let count = new Date(date);
count.setDate(count.getDate() - 1);
setDate(count);
};
return loading ? (
<Spinner />
) : (
<div className='main-grid'>
<div className='posts-grid'>
<h1 className='large text-primary'>Ideas</h1>
<div className='posts'>
<div className='post-dummy'>
<InfiniteScroll
dataLength={posts.length}
pageStart={0}
loadMore={getPosts}
hasMore={posts && posts.length < 10}
loader={
<div className='loader' key={0}>
Loading ...
</div>
}
>
{posts
.sort((a, b) =>
a.likes.length > b.likes.length
? -1
: b.likes.length > a.likes.length
? 1
: 0
)
.map(post => (
<PostItem key={post._id} post={post} />
))}
</InfiniteScroll>
</div>
</div>
</div>
<div className='right-panel-grid'>
<SmallAbout />
<UserItem />
<TopDiscussion posts={posts} />
<div
className='fb-group'
data-href='https://www.facebook.com/groups/ideatoshare/'
data-width='350'
data-show-social-context='true'
data-show-metadata='false'
></div>
<iframe
title='producthunt'
style={{ border: 'none' }}
src='https://cards.producthunt.com/cards/posts/168618?v=1'
width='350'
height='405'
frameBorder='0'
scrolling='no'
allowFullScreen
></iframe>
</div>
</div>
);
};
Posts.propTypes = {
getPostsByDate: PropTypes.func.isRequired,
post: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
post: state.post
});
export default connect(
mapStateToProps,
{ getPostsByDate }
)(Posts);
Post reducer
import {
GET_POSTS,
POST_ERROR,
UPDATE_LIKES,
UPDATE_LIKE,
UPDATE_COMMENT_LIKES,
DELETE_POST,
ADD_POST,
GET_POST,
ADD_COMMENT,
REMOVE_COMMENT,
ADD_SUB_COMMENT,
REMOVE_SUB_COMMENT,
UPDATE_STATUS
} from '../actions/types';
const initialState = {
posts: [],
post: null,
loading: true,
error: {}
};
export default function(state = initialState, action) {
const { type, payload } = action;
switch (type) {
case GET_POSTS:
return {
...state,
posts: [...state.posts, ...payload],
// posts: payload,
loading: false
};
case GET_POST:
return {
...state,
post: payload,
loading: false
};
case ADD_POST:
return {
...state,
post: payload,
// posts: [payload, ...state.posts],
loading: false
};
case POST_ERROR:
return {
...state,
error: payload,
loading: false
};
case UPDATE_COMMENT_LIKES:
return {
...state,
post: {
...state.post,
comments: payload
},
loading: false
};
case UPDATE_LIKES:
return {
...state,
posts: state.posts.map(post =>
post._id === payload.id ? { ...post, likes: payload.likes } : post
),
loading: false
};
case UPDATE_LIKE:
return {
...state,
post: { ...state.post, likes: payload },
loading: false
};
case UPDATE_STATUS:
return {
...state,
posts: state.posts.map(post =>
post._id === payload.id ? { ...post, status: payload.status } : post
),
loading: false
};
case DELETE_POST:
return {
...state,
posts: state.posts.filter(post => post._id !== payload),
loading: false
};
case ADD_COMMENT:
return {
...state,
// payload is all the comments
post: { ...state.post, comments: payload },
loading: false
};
case ADD_SUB_COMMENT:
return {
...state,
// payload is all the comments of a post
post: { ...state.post, comments: payload },
loading: false
};
case REMOVE_COMMENT:
return {
...state,
post: {
...state.post,
comments: state.post.comments.filter(
comment => comment._id !== payload
),
loading: false
}
};
case REMOVE_SUB_COMMENT:
return {
...state,
post: {
...state.post,
comments: payload
// comments: state.post.comments.map(comment =>
// {
// if (comment._id === payload.commentId) {
// comment.subComments.filter(
// subcomment => subcomment._id === payload.subcommentId
// );
// }
// }
// )
},
loading: false
};
default:
return state;
}
}
Post action
//GetTodayPost
export const getPostsByDate = date => async dispatch => {
try {
const res = await axios.get(`/api/posts/${date}`);
dispatch({
type: GET_POSTS,
payload: res.data
});
} catch (err) {
dispatch({
type: POST_ERROR,
payload: { msg: err.response.statusText, status: err.response.status }
});
}
};
post API
router.get('/:date', async (req, res) => {
try {
const startOfToday = new Date(req.params.date);
const endOfToday = new Date(req.params.date);
endOfToday.setDate(endOfToday.getDate() + 1);
const posts = await Post.find({
date: { $gte: startOfToday, $lte: endOfToday }
}).sort({
date: -1
});
res.json(posts);
} catch (err) {
console.error(err.message);
res.send(500).send('Server Error');
}
});
Edit: I have updated your repo with a working example.. Your issue is that your API is not 'unlimited', as you claimed, and you do in fact need to check if all posts have been loaded or not.. Using the example I supplied along with the updates I made to your repo, you should be able to figure out things from here.
Ok.. so after some testing with InfiniteScroll, this appears to be happening because your hasMore property always equals true... You have to specify some type of condition so that InfiniteScroll knows when to, and when not to, load more data.
I got the same error as you before adding a check, which tells InfiniteScroll that there is no more data to load.
I have built the following example to show how to use InfiniteScroll
You can view a live demo here
PostsContainer.js
import React, { useState, useEffect } from "react";
import Posts from "./Posts";
import InfiniteScroll from "react-infinite-scroller";
const loadingStyle = {
textAlign: "center",
fontSize: "48px",
color: "red"
};
function PostsContainer({ url, itemsToDisplay = 5 }) {
const [data, setData] = useState();
const [shownData, setShownData] = useState();
useEffect(() => {
(async () => {
let items = await fetchPosts(url);
let itemsToShow = selectNItems(items, itemsToDisplay);
setShownData(itemsToShow);
setData(items);
})();
}, [url]);
async function fetchPosts(url) {
let res = await fetch(url);
return await res.json();
}
const selectNItems = (obj, n) => {
return obj.slice(0, n);
}
const loadMorePosts = () => {
let items =
data &&
shownData &&
selectNItems(data, shownData.length + itemsToDisplay)
setShownData(items);
};
return (
<InfiniteScroll
pageStart={0}
loadMore={loadMorePosts}
hasMore={data && shownData && data.length > shownData.length}
loader={<div style={loadingStyle}>Loading ...</div>}
useWindow={true}
>
<Posts posts={shownData} />
</InfiniteScroll>
);
}
export default PostsContainer;
Posts.js
import React from 'react';
import Post from './Post';
const headingStyle = {
textAlign: 'center',
}
function Posts({ posts }) {
return(
<div>
<h1 style={headingStyle}>Posts</h1>
{posts && posts.length > 0 && posts.map((p, i) => <Post key={i} data={p} index={i} />)}
</div>
);
}
export default Posts;
Post.js
import React from "react";
const containerStyle = {
border: "1px solid black",
margin: "10px auto",
maxWidth: "50vw",
padding: '0px 10px 0px 0px'
};
const postHeaderStyle = {
textAlign: "center",
padding: "0px"
};
function Post({ data, index }) {
return (
<div style={containerStyle}>
{index !== "" && <h3 style={postHeaderStyle}>Post #{index}</h3>}
<ul>
<li>
<b>userId:</b> {data.userId}
</li>
<li>
<b>id:</b> {data.id}
</li>
<li>
<b>title:</b> {data.title}
</li>
<li>
<b>body:</b> {data.body}
</li>
</ul>
</div>
);
}
export default Post;
index.js
import React from "react";
import { render } from "react-dom";
import PostsContainer from "./Components/PostsContainer";
function App() {
return (
<PostsContainer
itemsToDisplay={5}
url="https://jsonplaceholder.typicode.com/posts"
/>
);
}
render(<App />, document.getElementById("root"));
Related
I have a Card component that shows a product and then a compare check box. If this is checked then the the item should reflect that in all areas it is shown (with a checked box). This is fine from page to page however there are a few spots that item may be displayed on a carousel and on a list for relative items. So then when that list item is clicked the same item in the carousel on the same 'page' does not update until we refresh. The code is fairly large but we have a button component inside the card component which has a function in the useEffect to watch for changes (which works fine until the item is shown twice on one page) but again it wont update unless we refresh.
I'm sharing the code for the compare button... that's really where I feel the changes are needed:
BUTTON COMPONENT
export interface LikeCompareButtonProps {
products: any[];
}
export default function LikeCompareButton({ products }: LikeCompareButtonProps) {
const dispatch = useDispatch();
const [checked, setChecked] = useState(false);
const checkProducts: any[] = useAppSelector((state) => state?.compareItems?.products)
|| [];
const checkForChange = (products: any) => {
if (compareView) {
setChecked(true);
}
if (!compareView) {
const checked = checkProducts?.find((x) => x.id === products?.id);
if (checked) {
setChecked(true);
} else {
setChecked(false);
}
}
};
const handleChange = (products: {}) => {
if (!checked) {
dispatch(setCompareItem(products));
dispatch(compareItemIncreaseCounter());
setChecked(true);
}
if (checked) {
dispatch(deleteCompareItem(products));
dispatch(compareItemDecreaseCounter());
setChecked(false);
}
getCompareItem();
};
useEffect(() => {
dispatch(getCompareItem());
dispatch(getCompareCounter());
checkForChange(products);
}, [checked]);
// hoping checked here would trigger every time it changes....
// parent component is card one. it maps through an Array -> products
return (
<div>
<button onClick={() => handleChange(products)}>
<div>
{checked && <CheckIcon className="text-white font-bold absolute h-4 w-4" />}
</div>
{!checked ? "Compare" : "Remove"}
</button>
</div>
);
}
CARD COMPONENT import Link from "next/link";
import LikeCompareButton from "../../button/like-compare-button";
export interface SmallProductCardProps {
products: any[];
}
export default function SmallProductCard({products}:
SmallProductCardProps) {
return (
<>
{products?.map((product: any, index: number) => (
<div key={product.id} >
<Link
href={
pathname: `/store/category/d/${product.id}`,
query: { item: product.id }
}}>
<a>
<img
src="http://www.innovativeengsystems.com/wp-c
ontent/uploads/2017/11/no-img-portrait.png"
alt={product.id}
/>
)}
</a>
</Link>
<LikeCompareButton
products={products[index]}
/>
</div>
))}
</>
);
}
HOME: WHERE PRODUCTS IS DEFINED
const Home: NextPage = () => {
const dispatch = useDispatch();
const products: any[] = useAppSelector((state) =>
state?.products?.products?.data);
useEffect(() => {
dispatch(getProductList()); //API RENDERED DATA
}, []);
return (
<>
<SectionLayout hero={true}>
<InnerGridViewLayout colSize={"col-span-4 grid-cols-2 grid-
rows-2 pt-0"}>
<SmallProductCard
products={products}
/>
</InnerGridViewLayout>
</SectionLayout>
</>
);
};
export default Home;
Adding REDUX work
ACTIONS
export const setCompareItem = (products: any) => {
return {
type: SET_COMPARE_ITEM,
payload: products
};
};
export const setCompareItemSuccessful = (products: any) => {
localStorage.setItem("item", JSON.stringify(products.payload));
const existingItems: any[] =
JSON.parse(localStorage.getItem("compareItems")!) ?? [];
existingItems?.push(products.payload);
localStorage.setItem("compareItems",
JSON.stringify(existingItems));
return {
type: SET_COMPARE_ITEM_SUCCESSFUL,
payload: products
};
};
export const getCompareItem = () => {
return {
type: GET_COMPARE_ITEM,
payload: []
};
};
export const getCompareItemSuccessful = () => {
var products: any[] = JSON.parse(localStorage.getItem("compareItems")!);
return {
type: GET_COMPARE_ITEM_SUCCESSFUL,
payload: products
};
};
REDUCER
import {
ERROR_COMPARE,
SET_COMPARE_ITEM,
SET_COMPARE_ITEM_SUCCESSFUL,
GET_COMPARE_ITEM,
GET_COMPARE_ITEM_SUCCESSFUL,
DELETE_COMPARE_ITEM
} from "./actionTypes";
const compareItemsState = {
products: [],
loading: false,
message: ""
};
const reducer = (state = compareItemsState, action) => {
switch (action.type) {
case SET_COMPARE_ITEM:
state = {
...state,
products: [action.payload],
message: "item added"
};
break;
case SET_COMPARE_ITEM_SUCCESSFUL:
state = {
...state,
products: [action.payload],
message: "set item success"
};
break;
case GET_COMPARE_ITEM:
state = {
...state,
products: action.payload,
message: "item found success"
};
break;
case GET_COMPARE_ITEM_SUCCESSFUL:
state = {
...state,
products: action.payload,
message: "found item"
};
break;
case DELETE_COMPARE_ITEM:
state = {
...state,
products: action.payload,
message: "item deleted"
};
break;
case ERROR_COMPARE:
state = {
...state,
compareCounter: state.compareCounter,
message: "Error adding message"
};
break;
default:
state = { ...state };
break;
}
return state;
};
export default reducer;
SAGA.tsx
import { takeEvery, fork, put, all, call } from "redux-
saga/effects";
import {
SET_COMPARE_ITEM,
SET_COMPARE_ITEM_SUCCESSFUL,
DELETE_COMPARE_ITEM,
GET_COMPARE_ITEM_SUCCESSFUL,
GET_COMPARE_ITEM,
} from "./actionTypes";
import {
compareError,
setCompareItem,
setCompareItemSuccessful,
deleteCompareItem,
getCompareItem,
getCompareItemSuccessful,
} from "./actions";
function* setCompareItems(product: any) {
try {
yield put(setCompareItem(product));
} catch (error: any) {
yield put(compareError("Item not added, please try again."));
}
}
function* setCompareItemsSuccessful(products: any) {
try {
yield put(setCompareItemSuccessful(products));
} catch (error: any) {
yield put(compareError("Item not removed, please try
again."));
}
}
function* getCompareItems() {
try {
yield put(getCompareItem());
} catch (error: any) {
yield put(compareError("please try again."));
}
}
function* getCompareItemsSuccessful() {
try {
yield put(getCompareItemSuccessful());
} catch (error: any) {
yield put(compareError("please try again."));
}
}
function* setDeleteItems(products: any) {
try {
yield put(deleteCompareItem(products));
} catch (error: any) {
yield put(compareError("Item removed"));
}
}
export function* watchSetCompareItemSuccess() {
yield takeEvery(SET_COMPARE_ITEM_SUCCESSFUL, setCompareItems);
}
export function* watchSetCompareItem() {
yield takeEvery(SET_COMPARE_ITEM, setCompareItemsSuccessful);
}
export function* watchGetCompareItemSuccess() {
yield takeEvery(GET_COMPARE_ITEM_SUCCESSFUL, getCompareItems);
}
export function* watchGetCompareItem() {
yield takeEvery(GET_COMPARE_ITEM, getCompareItemsSuccessful);
}
export function* watchDeleteCompareItem() {
yield takeEvery(DELETE_COMPARE_ITEM, setDeleteItems);
}
function* compareItemStoreSaga() {
yield all([
fork(watchSetCompareItem),
fork(watchGetCompareItem),
]);
}
export default compareItemStoreSaga;
I am using MERN and Redux.
I have a clickHandler function that calls a findAuthor function which is imported from my actions. This finds a user by their id and returns it. I have added the user to the global state. I want to then retrieve the user and add their name to local state but i can't get this working. I keep getting this error TypeError: this.props.subAuthor is undefined. What am i missing here? When i try just printing to console i get no object showing until the second click. How do i get it t update straight away?
import React, { Component } from "react";
import PropTypes from "prop-types";
import GoogleSearch from "./GoogleSearch";
import { connect } from "react-redux";
import { fetchSubjects } from "../../actions/subject";
import { fetchComments } from "../../actions/comment";
import { updateSubject } from "../../actions/subject";
import { getUser } from "../../actions/authActions";
class Subject extends Component {
// on loading the subjects and comments
// are fetched from the database
componentDidMount() {
this.props.fetchSubjects();
this.props.fetchComments();
}
constructor(props) {
super(props);
this.state = {
// set inital state for subjects
// description, summary and comments all invisible
viewDesription: -1,
viewSummary: -1,
comments: [],
name: "",
};
}
componentWillReceiveProps(nextProps) {
// new subject and comments are added to the top
// of the arrays
if (nextProps.newPost) {
this.props.subjects.unshift(nextProps.newPost);
}
if (nextProps.newPost) {
this.props.comments.unshift(nextProps.newPost);
}
}
clickHandler = (id) => {
// when a subject title is clicked pass in its id
// and make the description and comments visible
const { viewDescription } = this.state;
this.setState({ viewDescription: viewDescription === id ? -1 : id });
// add relevant comments to the state
var i;
var temp = [];
for (i = 0; i < this.props.comments.length; i++) {
if (this.props.comments[i].subject === id) {
temp.unshift(this.props.comments[i]);
}
}
this.setState({
comments: temp,
});
// save the subject id to local storage
// this is done incase a new comment is added
// then the subject associated with it can be retrieved
// and added as a property of that comment
localStorage.setItem("passedSubject", id);
//testing getUser
this.findAuthor(id); // this updates the tempUser in state
this.setState({ name: this.props.subAuthor.name });
};
// hovering on and off subjects toggles the visibility of the summary
hoverHandler = (id) => {
this.setState({ viewSummary: id });
};
hoverOffHandler = () => {
this.setState({ viewSummary: -1 });
};
rateHandler = (id, rate) => {
const subject = this.props.subjects.find((subject) => subject._id === id);
// when no subject was found, the updateSubject won't be called
subject &&
this.props.updateSubject(id, rate, subject.noOfVotes, subject.rating);
alert("Thank you for rating this subject.");
};
// take in the id of the subject
// find it in the props
// get its author id
// call the getUser passing the author id
findAuthor(id) {
console.log("Hitting findAuthor function");
const subject = this.props.subjects.find((subject) => subject._id === id);
const authorId = subject.author;
console.log(authorId);
this.props.getUser(authorId);
}
render() {
const subjectItems = this.props.subjects.map((subject) => {
// if the state equals the id set to visible if not set to invisible
var view = this.state.viewDescription === subject._id ? "" : "none";
var hover = this.state.viewSummary === subject._id ? "" : "none";
var comments = this.state.comments;
var subjectAuthor = this.state.name;
return (
<div key={subject._id}>
<div className="subjectTitle">
<p
className="title"
onClick={() => this.clickHandler(subject._id)}
onMouseEnter={() => this.hoverHandler(subject._id)}
onMouseLeave={() => this.hoverOffHandler()}
>
{subject.title}
</p>
<p className="rate">
Rate this subject:
<button onClick={() => this.rateHandler(subject._id, 1)}>
1
</button>
<button onClick={() => this.rateHandler(subject._id, 2)}>
2
</button>
<button onClick={() => this.rateHandler(subject._id, 3)}>
3
</button>
<button onClick={() => this.rateHandler(subject._id, 4)}>
4
</button>
<button onClick={() => this.rateHandler(subject._id, 5)}>
5
</button>
</p>
<p className="rating">
Rating: {(subject.rating / subject.noOfVotes).toFixed(1)}/5
</p>
<p className="summary" style={{ display: hover }}>
{subject.summary}
</p>
</div>
<div className="subjectBody " style={{ display: view }}>
<div className="subjectAuthor">
<p className="author">
Subject created by: {subjectAuthor}
<br /> {subject.date}
</p>
</div>
<div className="subjectDescription">
<p className="description">{subject.description}</p>
</div>
<div className="subjectLinks">Links:</div>
<div className="subjectComments">
<p style={{ fontWeight: "bold" }}>Comments:</p>
{comments.map((comment, i) => {
return (
<div key={i} className="singleComment">
<p>
{comment.title}
<br />
{comment.comment}
<br />
Comment by : {comment.author}
</p>
</div>
);
})}
<a href="/addcomment">
<div className="buttonAddComment">ADD COMMENT</div>
</a>
</div>
</div>
</div>
);
});
return (
<div id="Subject">
<GoogleSearch />
{subjectItems}
</div>
);
}
}
Subject.propTypes = {
fetchSubjects: PropTypes.func.isRequired,
fetchComments: PropTypes.func.isRequired,
updateSubject: PropTypes.func.isRequired,
getUser: PropTypes.func.isRequired,
subjects: PropTypes.array.isRequired,
comments: PropTypes.array.isRequired,
newPost: PropTypes.object,
subAuthor: PropTypes.object,
};
const mapStateToProps = (state) => ({
subjects: state.subjects.items,
newSubject: state.subjects.item,
comments: state.comments.items,
newComment: state.comments.item,
subAuthor: state.auth.tempUser[0],
});
// export default Subject;
export default connect(mapStateToProps, {
fetchSubjects,
fetchComments,
updateSubject, // rate subject
getUser, // used for getting author name
})(Subject, Comment);
I'd like to offer an alternative solution to the current code you have been writing so far. I know this is not codereview (and it wouldn't be on topic there, unless it is actually working code), but still, I would like to show you a different way of dividing up your components.
From what I see, you have many components, currently all jampacked in to one very large component. This can complicate things on the long run, and if you can, you should avoid it.
As I see it from the code you have posted, you really have several components, which I divided in:
Subject
Comment
User
Rating
RatingViewer
By dividing your now large component, you are making it easier to handle the data for one component at a later time and reuse the components you are making. You might want to reuse some of these components.
For the purpose of an alternative solution, I created a very quick and basic demo on how you might refactor your code. This is only a suggestion, in the hope that it will also solve your current problem.
The problem you are having is that you want to load that data, and use it directly. Any fetch operation is however asynchronous, so after you have called this.props.getUser(authorId); your author gets added somewhere in your state, but it will not be available until fetching has been completed and your component gets re-rendered.
I hope the information in the demo can give you some insight, it might not be exactly matching your scenario, but it should give you an indication of what you could do differently.
// imports
const { Component } = React;
const { Provider, connect } = ReactRedux;
const { render } = ReactDOM;
const { createStore, combineReducers } = Redux;
// some fake db data
const db = {
comments: [
{ id: 1, subject: 2, user: 2, comment: 'Interesting book' },
{ id: 2, subject: 2, user: 3, comment: 'Is interesting the only word you know, you twit' }
],
subjects: [
{
id: 1,
title: 'Some interesting title',
summary: 'Some interesting summary / plot point',
author: 2,
rate: 0,
noOfVotes: 0
},
{
id: 2,
title: 'Some less interesting title',
summary: 'Some more interesting summary / plot point',
author: 1,
rate: 5,
noOfVotes: 2
}
],
authors: [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Doe' }
],
users: [
{ id: 1, name: 'user 1' },
{ id: 2, name: 'user 2' },
{ id: 3, name: 'user 3' }
]
};
// reducers
const authorReducer = ( state = {}, action ) => {
switch (action.type) {
case 'author/add':
return { ...state, [action.payload.id]: action.payload };
default:
return state;
}
};
const userReducer = ( state = {}, action ) => {
switch (action.type) {
case 'user/add':
return { ...state, [action.payload.id]: action.payload };
default:
return state;
}
};
const subjectReducer = ( state = {}, action ) => {
switch (action.type) {
case 'subject/retrieved':
return Object.assign( {}, ...action.payload.map( subject => ({ [subject.id]: subject }) ) );
case 'subject/add':
return { ...state, [action.payload.id]: action.payload };
case 'subject/update':
const { id } = action.payload;
return { ...state, [id]: action.payload };
default:
return state;
}
};
const commentReducer = ( state = [], action ) => {
switch (action.type) {
case 'comment/retrieved':
return action.payload.slice();
case 'comments/add':
return [...state, action.payload ];
default:
return state;
}
};
// create the store
const store = createStore( combineReducers({
users: userReducer,
authors: authorReducer,
comments: commentReducer,
subjects: subjectReducer
}) );
// some promise aware fetch methods
const fakeFetch = (entity, filter = null) => {
const entities = db[entity];
return Promise.resolve( (filter ? entities.filter( filter ) : entities).map( e => ({...e}) ) );
}
const fakeUpdate = (entity, id, updatedValue ) => {
const targetEntity = db[entity].find( e => e.id === id );
if (!targetEntity) {
return Promise.reject();
}
Object.assign( targetEntity, updatedValue );
return Promise.resolve( { ...targetEntity } );
}
// separate components
class App extends Component {
render() {
return <Subjects />;
}
}
// subjects component
// cares about retrieving the subjects and displaying them
class SubjectsComponent extends Component {
componentDidMount() {
this.props.fetchSubjects();
}
render() {
const { subjects } = this.props;
if (!subjects || !subjects.length) {
return <div>Loading</div>;
}
return (
<div>
{ subjects.map( subject => <Subject key={subject.id} subject={subject} /> ) }
</div>
);
}
}
// subject component
// displays a subject and fetches the comments for "all" subjects
// this should probably only fetch its own comments, but then reducer has to be changed aswell
// to be aware of that
class SubjectComponent extends Component {
componentDidMount() {
this.props.fetchComments();
}
render() {
const { subject } = this.props;
return (
<div className="subject">
<h1>{ subject.title }<RateView subject={subject} /></h1>
<p>{ subject.summary }</p>
<Rate subject={subject} />
<h2>Comments</h2>
{ this.props.comments && this.props.comments.map( comment => <Comment key={comment.id} comment={comment} /> ) }
</div>
);
}
}
// Just displays a comment and a User component
const Comment = ({ comment }) => {
return (
<div className="comment">
<p>{ comment.comment }</p>
<User id={comment.user} />
</div>
);
}
// User component
// fetches the user in case he hasn't been loaded yet
class UserComponent extends Component {
componentDidMount() {
if (!this.props.user) {
this.props.fetchUser( this.props.id );
}
}
render() {
return <span className="user">{ this.props.user && this.props.user.name }</span>;
}
}
// shows the current rating of a post
const RateView = ({ subject }) => {
if (subject.noOfVotes === 0) {
return <span className="rating">No rating yet</span>;
}
const { rate, noOfVotes } = subject;
return <span className="rating">Total rating { (rate / noOfVotes).toFixed(1) }</span>;
}
// enables voting on a subject, can be triggered once per rendering
// this should truly be combined with the user who rated the subject, but it's a demo
class RateComponent extends Component {
constructor() {
super();
this.onRateClicked = this.onRateClicked.bind( this );
this.state = {
hasRated: false,
rateValue: -1
};
}
onRateClicked( e ) {
const userRate = parseInt( e.target.getAttribute('data-value') );
const { subject } = this.props;
this.setState({ hasRated: true, rateValue: userRate }, () => {
this.props.updateRate( { ...subject, rate: subject.rate + userRate, noOfVotes: subject.noOfVotes + 1 } );
});
}
render() {
if (this.state.hasRated) {
return <span className="user-rate">You rated this subject with { this.state.rateValue }</span>;
}
return (
<div>
{ [1, 2, 3, 4, 5].map( value => <button type="button" onClick={ this.onRateClicked } data-value={value} key={value}>{ value }</button> ) }
</div>
);
}
}
// connecting all the components to the store, with their states and dispatchers
const Subjects = connect(
state => ({ subjects: Object.values( state.subjects ) }),
dispatch => ({
fetchSubjects() {
return fakeFetch('subjects').then( result => dispatch({ type: 'subject/retrieved', payload: result }) );
}
}))( SubjectsComponent );
// ownProps will be used to filter only the data required for the component that it is using
const Subject = connect(
(state, ownProps) => ({ comments: state.comments.filter( comment => comment.subject === ownProps.subject.id ) }),
dispatch => ({
fetchComments() {
return fakeFetch('comments' ).then( result => dispatch({ type: 'comment/retrieved', payload: result }) );
}
}))( SubjectComponent );
const User = connect(
(state, ownProps) => ({ user: state.users[ownProps.id] }),
dispatch => ({
fetchUser( id ) {
return fakeFetch('users', user => user.id === id).then( result => dispatch({ type: 'user/add', payload: result[0] }) );
}
}))( UserComponent );
const Rate = connect( null, dispatch => ({
updateRate( updatedSubject ) {
return fakeUpdate('subjects', updatedSubject.id, updatedSubject).then( updated => dispatch({ type: 'subject/update', payload: updated }) );
}
}))( RateComponent );
// bind it all together and run the app
const targetElement = document.querySelector('#container');
render( <Provider store={store}><App /></Provider>, targetElement );
.user {
font-style: italic;
font-size: .9em;
}
.comment {
padding-left: 10px;
background-color: #efefef;
border-top: solid #ddd 1px;
}
h1, h2 {
font-size: .8em;
line-height: .9em;
}
.rating {
padding: 5px;
display: inline-block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js" integrity="sha512-SUJujhtUWZUlwsABaZNnTFRlvCu7XGBZBL1VF33qRvvgNk3pBS9E353kcag4JAv05/nsB9sanSXFbdHAUW9+lg==" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js" integrity="sha512-SYsXmAblZhruCNUVmTp5/v2a1Fnoia06iJh3+L9B9wUaqpRVjcNBQsqAglQG9b5+IaVCfLDH5+vW923JL5epZA==" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.1/react-redux.min.js" integrity="sha512-Ae6lzX7eAwqencnyfCtoAf2h3tQhsV5DrHiqExqyjKrxvTgPHwwOlM694naWdO2ChMmBk3by5oM2c3soVPbI5g==" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js" integrity="sha512-P36ourTueX/PrXrD4Auc1kVLoTE7bkWrIrkaM0IG2X3Fd90LFgTRogpZzNBssay0XOXhrIgudf4wFeftdsPDiQ==" crossorigin="anonymous"></script>
<div id="container"></div>
I am building a social media application using the MERN stack and Redux as the state manager. I have a feed component which renders PostItem components which display the post and allow for actions such as liking, and commenting. I also have a Post component that renders the same PostItem component that opens when the user clicks the comment button on the PostItem component in the feed. When I like a post via the feed component it receives the updated props and rerenders the component showing the changes. However when I click the like button in the Post component it updates the Redux store but does not receive the updated props.
Feed.js
import React, { Fragment, useEffect } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { getPosts } from '../../actions/post';
// Components
import UserProfileCard from './UserProfileCard';
import PostForm from './PostForm';
import Footer from '../layout/Footer';
import PostItem from '../posts/PostItem';
import Spinner from '../layout/Spinner';
const Feed = ({ getPosts, post: { posts, loading } }) => {
//Same as component did mount
useEffect(() => {
getPosts();
}, [getPosts]);
return (
<Fragment>
<div className='main-container mt-3'>
<div className='container'>
<div className='row'>
<UserProfileCard />
<PostForm />
</div>
{loading ? (
<Spinner />
) : (
posts.map(post => <PostItem key={post._id} post={post} />)
)}
</div>
</div>
<Footer />
</Fragment>
);
};
Feed.propTypes = {
getPosts: PropTypes.func.isRequired,
post: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
post: state.post
});
export default connect(mapStateToProps, { getPosts })(Feed);
Post.js
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { getPost } from '../../actions/post';
import { Link } from 'react-router-dom';
// Components
import Spinner from '../layout/Spinner';
import PostItem from './PostItem';
import PostCommentForm from './PostCommentForm';
import Comment from './Comment';
// Assets
import { ArrowLeft } from 'react-feather';
const Post = ({ getPost, post: { post, loading }, match }) => {
useEffect(() => {
// get id from url in params for getPost function
getPost(match.params.id);
}, [getPost]);
return loading || post === null ? (
<Spinner />
) : (
<div className='main-container mt-3'>
<div className='container'>
<div className='row'>
{/* TODO ADD BROWSER HISTORY FUNCTIONALITY TO ALLOW USER TO GO BACK TO PROFILE OR FEED */}
<Link className='mb-1' to='/feed'>
<button className='btn btn-logo-color'>
<ArrowLeft />
</button>
</Link>
<PostItem key={post._id} post={post} />
<PostCommentForm />
</div>
<Comment />
</div>
</div>
);
};
Post.propTypes = {
getPost: PropTypes.func.isRequired,
post: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
post: state.post
});
export default connect(mapStateToProps, { getPost })(Post);
PostItem.js
import React from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import Moment from 'react-moment';
import { connect } from 'react-redux';
import { addLike, removeLike, deletePost } from '../../actions/post';
// Assets
import { ThumbsUp, ThumbsDown, MessageSquare, XCircle } from 'react-feather';
import avi from '../assets/default-avatar.png';
const PostItem = ({
auth,
post: { _id, text, firstname, lastname, user, likes, comments, date },
addLike,
removeLike,
deletePost
}) => {
return (
<div className='card mt-1 post'>
<div className='card-body'>
<div className='row'>
<div className='col-lg-2'>
<Link className='feed-link' to={`/profile/${user}`}>
<img src={avi} alt='avatar' className='avatar' />
<h5 className='card-title mt-2'>
{firstname} {lastname}
</h5>
</Link>
</div>
<div className='col-lg-10'>
<p className='card-text'>{text}</p>
<p className='text-muted post-date'>
<Moment format='LLL'>{date}</Moment>
</p>
<div className='post-buttons'>
<button
type='button'
className='btn btn-outline-primary mr-1'
onClick={e => addLike(_id)}
>
<ThumbsUp />
<span className='badge badge-light'>
{likes.length > 0 && <span>{likes.length}</span>}
</span>
</button>
<button
type='button'
className='btn btn-outline-danger mr-1'
onClick={e => removeLike(_id)}
>
<ThumbsDown />
</button>
{/* TODO ADD CONDITIONAL RENDERING TO REMOVE WHEN POST IS OPEN */}
<Link to={`/post/${_id}`}>
<button type='button' className='btn btn-outline-info mr-1'>
<MessageSquare />
<span className='badge badge-light'>
{comments.length > 0 && (
<span className='comment-count'>{comments.length}</span>
)}
</span>
</button>
</Link>
{!auth.loading && user === auth.user._id && (
<button
type='button'
className='btn btn-outline-danger mr-1'
onClick={() => deletePost(_id)}
>
<XCircle />
</button>
)}
</div>
</div>
</div>
</div>
</div>
);
};
PostItem.propTypes = {
post: PropTypes.object.isRequired,
auth: PropTypes.object.isRequired,
addLike: PropTypes.func.isRequired,
removeLike: PropTypes.func.isRequired,
deletePost: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
auth: state.auth
});
export default connect(mapStateToProps, { addLike, removeLike, deletePost })(
PostItem
);
Post Reducer
import {
GET_POSTS,
POST_ERROR,
UPDATE_LIKES,
DELETE_POST,
ADD_POST,
GET_POST,
ADD_COMMENT,
REMOVE_COMMENT
} from '../actions/types';
const initialState = {
posts: [],
post: null,
loading: true,
error: {}
};
export default function(state = initialState, action) {
const { type, payload } = action;
switch (type) {
case GET_POSTS:
return {
...state,
posts: payload,
loading: false
};
case GET_POST:
return {
...state,
post: payload,
loading: false
};
case ADD_POST:
return {
...state,
posts: [payload, ...state.posts],
loading: false
};
case DELETE_POST:
return {
...state,
posts: state.posts.filter(post => post._id !== payload),
loading: false
};
case POST_ERROR:
return {
...state,
error: payload,
loading: false
};
case UPDATE_LIKES:
return {
...state,
posts: state.posts.map(post =>
post._id === payload.postId ? { ...post, likes: payload.likes } : post
),
loading: false
};
case ADD_COMMENT:
return {
...state,
post: { ...state.post, comments: payload },
loading: false
};
case REMOVE_COMMENT:
return {
...state,
post: {
...state.post,
comments: state.post.comments.filter(
comment => comment._id !== payload
)
},
loading: false
};
default:
return {
...state
};
}
}
Post actions
import axios from 'axios';
import { setAlert } from './alert';
import {
GET_POSTS,
POST_ERROR,
UPDATE_LIKES,
DELETE_POST,
ADD_POST,
GET_POST,
ADD_COMMENT,
REMOVE_COMMENT
} from './types';
//Get posts
export const getPosts = () => async dispatch => {
try {
const res = await axios.get('/api/posts');
dispatch({
type: GET_POSTS,
payload: res.data
});
} catch (error) {
dispatch({
type: POST_ERROR,
payload: { msg: error.response.data.msg, status: error.response.status }
});
}
};
// Add like
export const addLike = postId => async dispatch => {
try {
const res = await axios.put(`/api/posts/like/${postId}`);
dispatch({
type: UPDATE_LIKES,
payload: { postId, likes: res.data }
});
} catch (error) {
dispatch({
type: POST_ERROR,
payload: { msg: error.response.data.msg, status: error.response.status }
});
}
};
// Remove like
export const removeLike = postId => async dispatch => {
try {
const res = await axios.put(`/api/posts/unlike/${postId}`);
dispatch({
type: UPDATE_LIKES,
payload: { postId, likes: res.data }
});
} catch (error) {
dispatch({
type: POST_ERROR,
payload: { msg: error.response.data.msg, status: error.response.status }
});
}
};
// Add Post
export const addPost = formData => async dispatch => {
const config = {
headers: {
'Content-Type': 'application/json'
}
};
try {
const res = await axios.post('/api/posts', formData, config);
dispatch({
type: ADD_POST,
payload: res.data
});
dispatch(setAlert('Post Created', 'success'));
} catch (error) {
dispatch({
type: POST_ERROR,
payload: { msg: error.response.data.msg, status: error.response.status }
});
}
};
// Delete Post
export const deletePost = id => async dispatch => {
try {
const res = await axios.delete(`/api/posts/${id}`);
dispatch({
type: DELETE_POST,
payload: id
});
dispatch(setAlert('Post Removed', 'success'));
} catch (error) {
dispatch({
type: POST_ERROR,
payload: { msg: error.response.data.msg, status: error.response.status }
});
}
};
//Get post
export const getPost = id => async dispatch => {
try {
const res = await axios.get(`/api/posts/${id}`);
dispatch({
type: GET_POST,
payload: res.data
});
} catch (error) {
dispatch({
type: POST_ERROR,
payload: { msg: error.response.data.msg, status: error.response.status }
});
}
};
// Add Comment
export const addComment = (postId, formData) => async dispatch => {
const config = {
headers: {
'Content-Type': 'application/json'
}
};
try {
const res = await axios.post(
`/api/posts/comment/${postId}`,
formData,
config
);
dispatch({
type: ADD_COMMENT,
payload: res.data
});
dispatch(setAlert('Comment Added', 'success'));
} catch (error) {
dispatch({
type: POST_ERROR,
payload: { msg: error.response.data.msg, status: error.response.status }
});
}
};
// Delete Comment
export const deleteComment = (postId, commentId) => async dispatch => {
try {
const res = await axios.delete(`/api/posts/comment/${postId}/${commentId}`);
dispatch({
type: REMOVE_COMMENT,
payload: commentId
});
dispatch(setAlert('Comment Removed', 'success'));
} catch (error) {
dispatch({
type: POST_ERROR,
payload: { msg: error.response.data.msg, status: error.response.status }
});
}
};
You're updating the posts array here which is what you use to render the PostItems in Feed.
case UPDATE_LIKES:
return {
...state,
posts: state.posts.map(post =>
post._id === payload.postId ? { ...post, likes: payload.likes } : post
),
loading: false
};
However in Post.js you use the Post object, not the Posts array. Post has not been updated by the UPDATE_LIKES action so your component doesn't re-render.
I'm trying to fetch data through Redux (with actions & reducers) and store it in a ReactTable
Here is the Table :
// MisleadLeadsTable
import React from "react";
import "react-table-v6/react-table.css";
import ReactTable from "react-table-v6";
import { connect } from "react-redux";
import {
getLeadsNotValid,
updateSpecificNotValidLead
} from "../../actions/leads";
import Spinner from "../layout/Spinner";
class MisleadLeadsTable extends React.Component {
constructor(props) {
super();
const { getLeadsNotValid } = props;
// Going to get data from the Server
// Call the Action and use the Reducer
getLeadsNotValid();
// Later put the data in the state
this.state = {
data: []
};
this.renderEditable = this.renderEditable.bind(this);
}
componentDidMount() {
// TODO
const { leadsNotValid } = this.props;
this.setState({
data: leadsNotValid
});
}
// Edit the cells
renderEditable(cellInfo) {
return (
<div
style={{ backgroundColor: "#fafafa" }}
contentEditable
suppressContentEditableWarning
onBlur={e => {
const data = [...this.state.data];
data[cellInfo.index][cellInfo.column.id] = e.target.innerHTML;
this.setState({ data });
}}
dangerouslySetInnerHTML={{
__html: this.state.data[cellInfo.index][cellInfo.column.id]
}}
/>
);
}
render() {
// loading data or not
const { loadingData } = this.props;
// This "data" should hold the fetched data from the server
const { data } = this.state;
return (
<div>
{loadingData ? (
<Spinner />
) : (
<div>
<ReactTable
data={data}
columns={[
{
Header: "Business Name",
accessor: "BusinessName"
// Cell: this.renderEditable
}
]}
defaultPageSize={10}
className="-striped -highlight"
/>
<br />
</div>
)}
</div>
);
}
}
const mapStateToProps = state => ({
loadingData: state.leadReducer.loadingData,
leadsNotValid: state.leadReducer.leadsNotValid
});
const mapDispatchToProps = { getLeadsNotValid, updateSpecificNotValidLead };
export default connect(mapStateToProps, mapDispatchToProps)(MisleadLeadsTable);
However when I try to store the data in the State (in componentDidMount) it always comes back empty , and when the table is being rendered it gets an empty array.
It is crucial to store the data in the State because I'm trying to implement an editable table.
The data is stored in leadsNotValid , and if I do :
<ReactTable
data={leadsNotValid} // Notice !! Changed this
columns={[
{
Header: "Business Name",
accessor: "BusinessName"
// Cell: this.renderEditable
}
]}
defaultPageSize={10}
className="-striped -highlight"
/>
Then the data is presented successfully to the user , however it's not in the State of the component.
How can I put the leadsNotValid in the State using setState ?
Here are the Action & Reducer if it's needed (THEY WORK GREAT !) :
Action :
import axios from "axios";
import {
REQUEST_LEADS_NOT_VALID,
REQUEST_LEADS_NOT_VALID_SUCCESS,
UPDATED_SUCCESSFULLY_A_NOT_VALID_LEAD_THAT_NOW_IS_VALID,
UPDATE_A_SINGLE_NOT_VALID_LEAD
} from "./types";
export const updateSpecificNotValidLead = updatedLead => async dispatch => {
dispatch({
type: UPDATE_A_SINGLE_NOT_VALID_LEAD
});
const config = {
headers: {
"Content-Type": "application/json"
}
};
const body = JSON.stringify({ updatedLead });
const res = await axios.post(
".......API/Something1/....",
body,
config
);
if (res !== null && res.data !== null) {
dispatch({
type: UPDATED_SUCCESSFULLY_A_NOT_VALID_LEAD_THAT_NOW_IS_VALID
});
}
};
export const getLeadsNotValid = () => async dispatch => {
dispatch({
type: REQUEST_LEADS_NOT_VALID
});
const res = await axios.get(".......API/Something2/....");
if (res !== null && res.data !== null) {
dispatch({
type: REQUEST_LEADS_NOT_VALID_SUCCESS,
payload: res.data
});
}
};
Reducer :
import {
GET_MAIN_LEADS_SUCCESS,
REQUEST_MAIN_LEADS,
RELOAD_DATA_MAIN_LEADS_TABLE,
REQUEST_LEADS_NOT_VALID,
REQUEST_LEADS_NOT_VALID_SUCCESS,
UPDATE_A_SINGLE_NOT_VALID_LEAD,
UPDATED_SUCCESSFULLY_A_NOT_VALID_LEAD_THAT_NOW_IS_VALID
} from "../actions/types";
const initialState = {
mainLeadsClients: [],
loadingData: null, // general loader
reloadMainLeadTable: 0,
reloadMisleadTable: 0,
leadsNotValid: []
};
export default function(state = initialState, action) {
const { type, payload } = action;
switch (type) {
case REQUEST_LEADS_NOT_VALID:
return {
...state,
loadingData: true
};
case REQUEST_LEADS_NOT_VALID_SUCCESS:
return {
...state,
loadingData: false,
leadsNotValid: payload
};
case UPDATE_A_SINGLE_NOT_VALID_LEAD:
return {
...state,
loadingData: true
};
case UPDATED_SUCCESSFULLY_A_NOT_VALID_LEAD_THAT_NOW_IS_VALID:
return {
...state,
reloadMisleadTable: state.reloadMisleadTable + 1,
loadingData: false
};
// ... more
default:
return state;
}
}
You may have to write super(props) instead of super() in order to access props in the constructor.
I'm using this boilerplate and am having issues where my container is not rerendering after my state has updated. I have two actions that get dispatched, 1) LOAD - dispatched during the start of the AJAX request and 2) LOAD_SUCCESS - once the data has been successfully returned.
The LOAD_SUCCESS action gets called from the reducer but nothing happens after that. This code used to work a couple weeks ago (project was on hold for a bit) but no longer works. Any thoughts ?
import superagent from 'superagent';
import config from '../../config';
const LOAD = 'cnnm/sectionFront/LOAD';
const LOAD_SUCCESS = 'cnnm/sectionFront/LOAD_SUCCESS';
const LOAD_FAIL = 'cnnm/sectionFront/LOAD_FAIL';
const initialState = {
loaded: false,
loading: false,
currentSection: null,
data: {}
};
export default function reducer(state = initialState, action = {}) {
switch (action.type) {
case LOAD:
return {
...state,
loading: true,
loaded: false
};
case LOAD_SUCCESS:
const result = {
...state,
loading: false,
loaded: true,
error: null,
currentSection: action.sectionID
};
result.data[action.sectionID] = action.data;
return result;
case LOAD_FAIL:
return {
...state,
loading: false,
loaded: true,
data: null,
error: action.error
};
default:
return state;
}
}
export function load() {
return (dispatch, getState) =>{
dispatch({
type: LOAD
});
const globalState = getState();
const endpoint = config.endpoints.sectionFronts[globalState.routing.locationBeforeTransitions.pathname];
if ( globalState.sectionFront.data[globalState.routing.locationBeforeTransitions.pathname] === undefined ){
superagent.get( endpoint )
.end( (err, resp) => {
if ( err ){
dispatch({
type: LOAD_FAIL,
error: err
});
}
else {
try {
const data = JSON.parse( resp.text );
dispatch({
type: LOAD_SUCCESS,
sectionID: globalState.routing.locationBeforeTransitions.pathname,
data
});
}
catch ( error ){
console.warn('Error trying to parse section front', error);
dispatch({
type: LOAD_FAIL,
error: error
});
}
}
});
}
else {
// Already have the data cached
dispatch({
type: LOAD_SUCCESS,
sectionID: globalState.routing.locationBeforeTransitions.pathname,
data: globalState.sectionFront.data[globalState.routing.locationBeforeTransitions.pathname]
});
}
};
}
Container code:
import React, {Component, PropTypes} from 'react';
import {connect} from 'react-redux';
import Helmet from 'react-helmet';
import Col from 'react-bootstrap/lib/Col';
import Row from 'react-bootstrap/lib/Row';
import { asyncConnect } from 'redux-async-connect';
import { load as loadSectionFront } from 'redux/modules/sectionFront';
import {bindActionCreators} from 'redux';
import { Loading } from 'components';
import Grid from 'react-bootstrap/lib/Grid';
import { cards as cardComponents } from 'components';
#asyncConnect([{
deferred: true,
promise: ({store: {dispatch, getState}}) => {
return dispatch(loadSectionFront());
}
}])
#connect(
state => {
return {
section: state.sectionFront.data,
currentSection: state.sectionFront.currentSection,
loaded: state.sectionFront.loaded,
loading: state.sectionFront.loading
};
},
dispatch => bindActionCreators( {loadSectionFront}, dispatch)
)
export default class SectionFront extends Component {
static propTypes = {
section: PropTypes.object,
loaded: PropTypes.bool,
loading: PropTypes.bool,
currentSection: PropTypes.string,
loadSectionFront: PropTypes.func,
}
mapSecionNameToPrettyName = ( sectionID ) => {
switch ( sectionID ){
case '/':
return 'Top News';
case '/video':
return 'Video';
case '/technology':
return 'Technology';
case '/media':
return 'Media';
case '/investing':
return 'Investing';
case '/news/economy':
return 'Economy';
case '/pf':
return 'Personal Finance';
case '/retirement':
return 'Retirement';
case '/autos':
return 'Autos';
case '/smallbusiness':
return 'Small Business';
case '/news/companies':
return 'Companies';
case '/luxury':
return 'Luxury';
case '/real_estate':
return 'Real Estate';
default:
return 'Not found';
}
}
render() {
const {section, loaded, currentSection, loading } = this.props;
const sectionName = this.mapSecionNameToPrettyName( currentSection );
const styles = require('./SectionFront.scss');
let cardNodes = '';
if (loaded){
cardNodes = section[currentSection].cards.map((card, index) => {
switch ( card.cardType ) {
case 'summary':
if ( card.images === undefined || card.cardStyle === 'text'){
return <cardComponents.SummaryTextComponent card={card} key={index} />;
}
else {
if ( card.cardStyle === 'default' ){
return <cardComponents.SummaryDefault card={card} key={index} />;
}
else if ( card.cardStyle === 'jumbo' || card.cardStyle === 'jumboBlack' ){
return <cardComponents.SummaryJumboComponent card={card} key={index} />;
}
else if ( card.cardStyle === 'jumboOverlay' ){
return <cardComponents.SummaryJumboOverlayComponent card={card} key={index} />;
}
}
break;
case 'marketIndices':
return <cardComponents.MarketIndicesComponent key={index} />;
case 'ad':
return <cardComponents.AdComponent card={card} key={index} />;
case 'bizDev':
return <cardComponents.BizDevComponent card={card} key={index} />;
default:
return ( <div key={index}> </div>);
}
});
}
return (
<div className={styles.sectionFront}>
{ loading ? <div className={styles.overlay}><Loading /></div> : null }
<Grid className={styles.container}>
<Row className={styles.sectionTitle}>
<Col className={ 'col-xs-12 ' + styles.sectionCol}>
<h1 className="container">{sectionName}</h1>
</Col>
</Row>
{cardNodes}
</Grid>
</div>
);
}
}
Per the Redux FAQ at http://redux.js.org/docs/FAQ.html#react-not-rerendering, 99.9% of the time the reason a component isn't re-rendering is because of a reducer directly mutating state. This line appears to be doing just that: result.data[action.sectionID] = action.data;. You need to return a cloned version of data inside of result as well.
Now, I would actually expect that the changes in "loading" and "loaded" would correctly cause the component to re-render even if the "data" reference stayed the same, but there might be something else I'm missing here.