React UseState hook causing infinite loop - reactjs

I am using ReactJs to grab an RSS news feed every 5 seconds to convert it into a JSON string to render it on the webpage. I am using both useEffect and useState hook for this purpose as I am passing the JSON string in the useState hook variable, however. It kind of works but it produces an infinite loop. I have searched through the fixes provided in stack overflow but I couldn't find the exact problem. Here is my code snippet.'
import React, {useEffect, useState} from 'react';
import Carousel from 'react-bootstrap/Carousel';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import {getNews} from "../../actions/news";
import Parser from 'rss-parser';
const NewsCarousel = ({getNews, news: {news, loading} }) => {
const [getFeed, setFeed] = useState({
feed: ''
});
useEffect(() => {
const interval = setInterval(() => {
getNews();
}, 5000);
return () => clearInterval(interval);
}, [getNews]);
const { feed } = getFeed;
const newsFeed = feed => setFeed({ ...getFeed, feed: feed });
let parser = new Parser();
parser.parseString(news, function(err, feed){
if (!err) {
newsFeed(feed);
} else {
console.log(err);
}
});
console.log(feed);
return (
<div className="dark-overlay">
</div>
);
};
NewsCarousel.propTypes = {
getNews: PropTypes.func.isRequired,
news: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
news: state.news
});
export default connect(mapStateToProps, {getNews}) (NewsCarousel);
Its when I console.log my feed variable that's when I see in the console the infinite logs.
Below is my getNews Action
import axios from 'axios';
import { GET_NEWS, NEWS_FAIL } from "./types";
export const getNews = () => async dispatch => {
try{
const res = await axios.get('https://www.cbc.ca/cmlink/rss-
topstories');
dispatch({
type: GET_NEWS,
payload: res.data
})
} catch(err) {
dispatch({
type: NEWS_FAIL,
payload: { msg: err}
})
}
};

You need to parse your news only when there is a change in new props. Add another useEffect with news as a dependency so it will be called when the news changes and then update your state there.
import React, {useEffect, useState} from 'react';
import Carousel from 'react-bootstrap/Carousel';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import {getNews} from "../../actions/news";
import Parser from 'rss-parser';
const NewsCarousel = ({getNews, news: {news, loading} }) => {
const [getFeed, setFeed] = useState({
feed: ''
});
useEffect(() => {
const interval = setInterval(() => {
getNews();
}, 5000);
return () => clearInterval(interval);
}, [getNews]);
useEffect(() => {
const newsFeed = feed => setFeed({ ...getFeed, feed: feed });
const parser = new Parser();
parser.parseString(news, function(err, feed){
if (!err) {
newsFeed(feed);
} else {
console.log(err);
}
});
}, [news]);
return (
<div className="dark-overlay">
</div>
);
};
NewsCarousel.propTypes = {
getNews: PropTypes.func.isRequired,
news: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
news: state.news
});
export default connect(mapStateToProps, {getNews}) (NewsCarousel);

Related

Maximum update depth exceeded when implementing useLocalStorage

I'm implementing a localStorage on NextJs TypeScript by following https://upmostly.com/next-js/using-localstorage-in-next-js but I get an error
on the context provider repeatedly.
Here is my code.
// My implementation for the context
import { useLocalStorage } from '#/Hooks/useLocalStorage';
import { Invoice, Invoices } from '#/Types/invoice';
import { createContext, Dispatch, SetStateAction, useContext } from 'react';
export const defaultInvoiceValue: Invoice = {
title: '',
items: [],
note: '',
status: '',
};
export const InvoiceContext = createContext<Invoices>({
invoices: [defaultInvoiceValue],
});
export const SetInvoicesContext = createContext<
Dispatch<SetStateAction<Invoices>>
>((value) => {
console.log('Set invoice context', value);
});
export const useInvoices = () =>
useLocalStorage<Invoices>('invoice', { invoices: [defaultInvoiceValue] });
export const useInvoiceContext = () => {
return useContext(InvoiceContext);
};
export const useSetInvoiceContext = () => {
return useContext(SetInvoicesContext);
};
// Provider wrapper
import { InvoiceContext, SetInvoicesContext, useInvoices } from '#/Context/InvoiceContext';
import { PropsWithChildren } from 'react';
export const InvoicesContextProvider = ({ children }: PropsWithChildren) => {
const [invoices, setInvoices] = useInvoices();
return (
<InvoiceContext.Provider value={invoices}>
<SetInvoicesContext.Provider value={setInvoices}>
{children}
</SetInvoicesContext.Provider>
</InvoiceContext.Provider>
)
};
The default context works fine. useSetInvoiceContext() also doesn't work
I would recommend you to check if the value provided in "invoices" in the hook useInvoinces is actually the same as the current value, before calling setInvoices. This will help you to avoid the infinite rendering.
For example, if the title and the items are the same as the current invoices, it will not update the state of invoices.
Something like:
let areNewInvoicesSameAsCurrent = newInvoices?.items?.every(invoice => currentInvoices.items.includes(invoice) );
if(newInvoices.title !== currentInvoices.title && !areNewInvoicesSameAsCurrent){
setInvoices(newInvoices)
}

React: Getting Initial State in Class based compopnents

I have a react-native, redux app, and after upgrading I've started getting some warnings about lifecycle hooks. My code looks like below:
import React from 'react';
import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';
import { selectPosts} from '../redux/selectors/postSelector';
import { getPosts } from '../redux/actions/postActions';
class BasicScreen extends React.Component {
state = {
data: [],
myItems: [],
};
componentWillMount() {
this.getPosts();
}
componentDidMount() {
this.checkforItems();
}
getPosts = async () => {
// Call to a redux action
await this.props.getPosts();
};
checkforItems = async () => {
// myItems in initial state are set from data in
AsyncStorage.getItem('MyItems').then(item => {
if (item) {
this.setState({
myItems: JSON.parse(item),
});
} else {
console.log('No data.');
}
});
};
componentWillReceiveProps(nextProps) {
// Data comes from the redux action.
if (
nextProps.data &&
!this.state.data.length &&
nextProps.data.length !== 0
) {
this.setState({
data: nextProps.data,
});
}
}
render() {
return (
<View>/* A detailed view */</View>
)
}
}
const mapStateToProps = createStructuredSelector({
data: selectPosts,
});
const mapDispatchToProps = dispatch => ({
dispatch,
getPosts: () => dispatch(getPosts()),
});
export default connect(mapStateToProps, mapDispatchToProps)(BasicScreen);
To summarize, I was calling a redux action (this.getPosts()) from componentWillMount(), and then updating the state by props received in componentWillReceiveProps. Now both these are deprecated, and I am getting warnings that these are deprecated.
Apart from this, I am also setting some initial state by pulling some data from storage (this.checkforItems()). This gives me another warning - Cannot update a component from inside the function body of a different component.
To me it looks like the solution lies in converting this into a functional component, however, I'm stuck at how I will call my initial redux action to set the initial state.
UPDATE:
I converted this into a functional component, and the code looks as follows:
import React, { Fragment, useState, useEffect } from 'react';
import { connect } from 'react-redux';
import AsyncStorage from '#react-native-community/async-storage';
import { StyleSheet,
ScrollView,
View,
} from 'react-native';
import {
Text,
Right,
} from 'native-base';
import { createStructuredSelector } from 'reselect';
import {
makeSelectPosts,
} from '../redux/selectors/postSelector';
import { getPosts } from '../redux/actions/postActions';
const BasicScreen = ({ data, getPosts }) => {
const [myData, setData] = useState([]);
const [myItems, setItems] = useState([]);
const checkForItems = () => {
var storageItems = AsyncStorage.getItem("MyItems").then((item) => {
if (item) {
return JSON.parse(item);
}
});
setItems(storageItems);
};
useEffect(() => {
async function getItems() {
await getPosts(); // Redux action to get posts
await checkForItems(); // calling function to get data from storage
setData(data);
}
getItems();
}, [data]);
return (
<View>
<>
<Text>{JSON.stringify(myItems)}</Text>
<Text>{JSON.stringify(myData)}</Text>
</>
</View>
);
}
const mapStateToProps = createStructuredSelector({
data: makeSelectPosts,
});
const mapDispatchToProps = dispatch => ({
dispatch,
getPosts: () => dispatch(getPosts()),
});
export default connect(mapStateToProps, mapDispatchToProps)(BasicScreen);
It works, but the problem is that the first Text - {JSON.stringify(myItems)} - it is rerendering continuously. This data is actually got using checkForItems(). I wanted the useEffect to be called again only when the data updates, but instead something else is happening.
Also, I noticed that setData is not being called correctly. The data becomes available through the prop (data), but not from the state (myData). myData just returns empty array.

Should I add async code in container component?

I'm making my first React-Redux project.
I wanna get data from getListAPI.
I checked console.log(data) in [GET_LIST_SUCCESS], and there was what I wanted.
But console.log(temp) in container, I expect 'data', it was just action object(only type exists).
How can I get the 'data'?
// container
import React from 'react';
import { useDispatch } from 'react-redux';
import Home from 'presentations/Home';
import * as homeActions from 'modules/home';
const HomeContainer = () => {
const dispatch = useDispatch();
const temp = dispatch(homeActions.getList());
console.log(temp);
return (
<Home />
);
}
export default HomeContainer;
// Redux module
import axios from 'axios';
import { call, put, takeEvery } from 'redux-saga/effects';
import { createAction, handleActions } from 'redux-actions';
function getListAPI() {
return axios.get('http://localhost:8000/');
}
const GET_LIST = 'home/GET_LIST';
const GET_LIST_SUCCESS = 'home/GET_LIST_SUCCESS';
const GET_LIST_FAILURE = 'home/GET_LIST_FAILURE';
export const getList = createAction(GET_LIST);
function* getListSaga() {
try {
const response = yield call(getListAPI);
yield put({ type: GET_LIST_SUCCESS, payload: response });
} catch (e) {
yield put({ type: GET_LIST_FAILURE, payload: e });
}
}
const initialState = {
data: {
id: '',
title: '',
created_at: '',
updated_at: '',
content: '',
view: '',
}
};
export function* homeSaga() {
yield takeEvery('home/GET_LIST', getListSaga);
}
export default handleActions(
{
[GET_LIST_SUCCESS]: (state, action) => {
const data = action.payload.data;
console.log(data);
return {
data
};
}
}, initialState
);
Maybe I need like async/await or Promise.then() or useCallback, etc in container?
Because I thought Redux-Saga handles async, but container isn't in Redux-Saga area.
So shouldn't I inject the container with async processing?
I wrote some code for test.
Expecting to receive other data in a few seconds.
// container
// const temp = dispatch(homeActions.getList());
let temp = dispatch(homeActions.getList());
let timer = setInterval(() => console.log(temp), 1000);
setTimeout(() => { clearInterval(timer); alert('stop');}, 50000);
Nothing changed.
It's just log action object(only type exists).
What am I missing?
dispatch() returns the action dispatched to the store (that's why the console.log(temp) shows the action itself).
You need to create a selector to fetch the data from the store and use the useSelector() hook:
// container
import React from 'react';
import { useDispatch } from 'react-redux';
import Home from 'presentations/Home';
import * as homeActions from 'modules/home';
const selectData = (state) => state.data
const HomeContainer = () => {
const dispatch = useDispatch();
const temp = useSelector(selectData)
dispatch(homeActions.getList());
// Do something with temp
return (
<Home />
);
}
export default HomeContainer;

React Redux not having context

Working on react web & using react-redux, facing an error (please refer below screenshot). When building application no errors, only navigates to that route getting it. Looks like this.props is not having access to redux context.
Don't know what am missing?
Same implementation is working fine for another component to list all users.
Products.js
import React, { Component } from "react";
import { connect } from "react-redux";
import { GetAllProducts } from "../../redux/actions/ProductsActions";
export class Products extends Component {
componentDidMount() {
this.props.GetAllProducts();
}
render() {
return (
<div className="col-span-12">
{(this.props.allProducts || []).map((product) => {
return <div>{product.productName}</div>;
})}
</div>
);
}
}
const mapStateToProps = (state) => ({
status: state.Products.status,
allProducts: state.Products.productList || [],
});
const mapDispatchToProps = (dispatch) => {
return {
GetAllProducts: () => dispatch(GetAllProducts("products")),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Products);
ProductsActions.js
import * as actions from "../actions/ActionTypes";
import axios from "axios";
export const GetAllProducts = (catagory) => {
return (dispatch) => {
dispatch({ type: actions.GET_PRODUCT_LIST_PENDING });
axios
.get(`http://localhost:5000/api/get/${catagory}`)
.then((res) => {
dispatch({
type: actions.GET_PRODUCT_LIST_SUCCESS,
payload: res.data,
});
})
.catch((err) => {
dispatch({
type: actions.GET_PRODUCT_LIST_FAILURE,
payload: err,
});
});
};
};
Thank you #El Aoutar Hamza, I've imported like this
Old code:
import { Products } from "./Products";
New code - Resolved
import Products from "./Products";
By removing the curly brackets the issue got resolved.

React-Redux component test fails when using async actions

I'm trying to test react-redux connected app which is having aync action to fetch data from API. But the test fails for some reason. what wrong i'm doing here?
AssertionError: expected { length: 0 } to have a length of 1 but got 0
posts.js(Redux Action)
import instance from "./../config/axiosconfig";
export const postList = () => {
return dispatch => {
return instance.get("/posts")
.then(res => {
const posts = res.data;
return dispatch({
type: "POST_LIST", posts
});
});
};
};
posts.js(React Component)
import React, { Component } from "react";
import { connect } from "react-redux";
import {postList} from "../actions/posts";
import PropTypes from "prop-types";
class Post extends Component {
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {
if (this.props.posts === undefined || this.props.posts.length === 0) {
const {dispatch} = this.props;
dispatch(postList());
}
}
render() {
let postLists = "";
if (this.props.posts) {
postLists = this.props.posts.map((list, i) => (
<li key = {i}>
{list.title}
</li>
));
}
return (
<div>
<h2>About</h2>
<p>Services</p>
<ol className="post-list">
{postLists}
</ol>
</div>
);
}
}
Post.propTypes = {
posts: PropTypes.array,
dispatch: PropTypes.func
};
const mapDispatchToProps = (dispatch) => ({ dispatch });
const mapStateToProps = (state) => {
return { posts: state.PostReducer ? state.PostReducer.posts : [] };
};
export default connect(mapStateToProps, mapDispatchToProps)(Post);
Post.test.js(Test for component)
import React from "react";
import Post from "client/components/post";
import { Provider } from 'react-redux';
import PostReducer from "client/reducers/posts";
import {mount, render, shallow} from 'enzyme'
import instance from "client/config/axiosconfig";
import { expect } from "chai";
import moxios from "moxios";
import { createStore, applyMiddleware, combineReducers } from 'redux';
import configureStore from "redux-mock-store";
import thunkMiddleware from "redux-thunk";
let store;
let wrapper;
describe("Post Component", () => {
beforeEach(() => {
moxios.install(instance);
});
afterEach(() => {
moxios.uninstall(instance);
});
it("has expected posts lists listed", async () => {
store = setupStore();
const payload = [{
body: "TEST",
id: 1,
title: "Test Title"
}];
moxios.wait(() => {
const request = moxios.requests.mostRecent();
request.respondWith({
status: 200,
response: payload
});
});
wrapper = mount(<Provider store={store}><Post/></Provider>);
expect(wrapper.find('.post-list li')).to.have.length(1);
});
function setupStore() {
return createStore(
combineReducers({ PostReducer }),
applyMiddleware(thunkMiddleware)
);
}
});
The request which you have stubbed may still be pending by the time make an assertion.
Try the following:
it("has expected posts lists listed", async () => {
store = setupStore();
const payload = [{
body: "TEST",
id: 1,
title: "Test Title"
}];
const promise = moxios.wait(() => {
const request = moxios.requests.mostRecent();
request.respondWith({
status: 200,
response: payload
});
});
const wrapper = mount(<Provider store={store}><Post/></Provider>);
await promise;
wrapper.update(); // May not be necessary
expect(wrapper.find('.post-list li')).to.have.length(1);
});

Resources