How to stop function again and again calling in render - javascript

I am new to react, I am getting data from redux, first, I get an object from accounts from redux, then I pass this to the function in redux and set a value in numReg in the reducer.
When I call a function by this.props.fetchAccountDetail(data) in actions its send a request to API and fetch the data from API and save it in reducer or store. When i call function in render by
this.getDataFromAccount(accountDetail.num), it goes in infinite loop.
I want data in a return, it should only run one time.
import React, { Component } from 'react'
import { fetchAccountDetail, } from '../../../actions'
class myclass extends Component {
state = {
num : ''
};
getAccounts = (data) => {
if (!data) { return; }
return data.find(item => item.id == this.props.match.params.id);
}
getDataFromAccount = (data) => {
this.props.fetchAccountDetail(data);
// This is a api , which provide the result agaisnt
// a num and set value in numReg in reducer
}
render() {
const { accounts, numReg } = this.props;
const accountDetail = this.getAccounts(accounts);
// Here i will get a match object like {id :1 , num :12345}
const test=this.getDataFromAccount(accountDetail.num)
// When i call this , it stucks in infinite loop , how can i get data only once when it render
console.log(test)
return (
<div />
);
}
}
const mapStateToProps = state => {
return { accounts : state.accounts.accounts | [{id :1 , num :12345} , {id :2 , num :535234}],
numReg : state.accounts.numReg
//Is a object containg the information of num from accounts
}
}
export default (compose(
withStyles(styles),
connect(mapStateToProps, { fetchAccountDetail,}))(myclass));
It should return data in variable test after fetching data from redux.

You should never call data fetching functions or functions which alter the state within render.
Render may be called multiple times if a parent rerenders or just its internal state changes.
Calling fetchAccountDetails in render updates the redux store. Redux will pass the new but equal data as props into your component.
That Component will rerender because its props changed and will call fetchAccountDetails again => loop. Render should only display data!!
For data fetching, 2 functions exist. componentDidMount which will be called after the component is visible. That would be a good place to call your fetch.
If you need a prop to fetch the data for e.g. an Id of some sort (fetch data for that Id), you would use componentDidUpdate in which you compare the new id and the old id to see if you need to fetch the data again.
You should read the docs and look at some tutorials.
Hope this helps.
Happy coding.

As Domino987 answered, you need to make use of lifecycle methods. Here's an example of how it might look:
componentDidMount() {
const { accounts } = this.props;
const accountDetail = this.getAccounts(accounts);
const accountData = this.getDataFromAccount(accountDetail.num)
this.setState({
account: {
accountDetail: accountDetail,
accountData: accountData
}
})
}
componentDidUpdate() {
const { accounts } = this.props;
const accountDetail = this.getAccounts(accounts);
const accountData = this.getDataFromAccount(accountDetail.num)
if (this.state.account.accountData !== this.getDataFromAccount(accountDetail.num)) {
this.setState({
account: {
accountDetail: accountDetail,
accountData: accountData
}
})
}
}

Related

React hook, wired issue when use useState, while if use setState work perfectly, how to solve it

dear community, I am facing a wired issue, and I don't know how to summary my situation in the question title, so I wonder if the question title is accurate enough.
I was trying to convert a class component to a hook component.
The class version code like this
async componentDidMount() {
const { dispatch, itemId } = this.props;
try {
if (itemId) {
await dispatch({
type: 'assignment/fetchSubmissionsByAssignment', //here to fetch submissions in props
payload: {
id: itemId
}
});
}
const { submissions } = this.props;
this.setState({
studentSubmissions: submissions,
});
} catch (error) {
throw error.message;
}
}
render() {
const { studentSubmissions } = this.state;
return (
<Table dataSource={studentSubmissions} />
)
}
export default SubmissionsDetail;
and in hook, it look like this
const [studentSubmissions, setStudentSubmissions] = useState([]);
useEffect(() => {
async function fetchSubmissions() {
const { dispatch, itemId } = props;
try {
if (itemId) {
await dispatch({
type: 'assignment/fetchSubmissionsByAssignment',
payload: {
id: itemId
}
});
}
const { submissions } = props;
setStudentSubmissions(submissions)
} catch (error) {
throw error.message;
}
};
fetchSubmissions()
}, []);
return (
<Table dataSource={studentSubmissions} />
)
export default SubmissionsDetail;
I omitted some code for better reading, like connect to redux store or others.
and the component is import in the parent file like this
import SubmissionsDetail from './SubmissionsDetail'
{assignmentIds.map((itemId) => {
<SubmissionsDetail itemId={itemId}/>
})}
it work perfect in class component, the expected result should return tables like this
However, when I change to use hook, the result return like this
or sometimes all data in tables become submissions3
I try to console.log(submissions) inside the try{...} block, when in class, the result is
which is correct, there have two assignments, the one have 4 submissions, another one have zero submission.
But the output in hook is different, the result is like this
either both have 4 submissions, either both have zero. That means one obj affect all other obj.
It seems like if useState change, it would influence other objs, that make me really confused. I think in the map method, each item is independent, right? If so, and how to explain why it work perfectly in class setState, but failed in hook useState?
I hope my question is clear enough, If you know how to describe my question in short, plz let me know, I would update the title, to help locate experts to answer.
Please don't hesitate to share your opinions, I really appreciate and need your help, many thanks!
Edit: You are probably going to want to rework the way you store the submission inside of the redux store if you really want to use the Hook Component. It seems like right now, submissions is just an array that gets overwritten whenever a new API call is made, and for some reason, the Class Component doesn't update (and it's suppose to update).
Sorry it's hard to make suggestions, your setup looks very different than the Redux environments I used. But here's how I would store the submissions:
// no submissions loaded
submissions: {}
// loading new submission into a state
state: {
...state,
sessions: {
...state.session,
[itemId]: data
}
}
// Setting the state inside the component
setStudentSubmissions(props.submissions[itemId])
And I think you will want to change
yield put({
type: 'getSubmissions',
payload: response.data.collections
});
to something like
yield put({
type: 'getSubmissions',
payload: {
data: response.data.collections,
itemId: id
});
If you want to try a "hack" you can maybe get a useMemo to avoid updating? But again, you're doing something React is not suppose to do and this might not work:
// remove the useEffect and useState, and import useMemo
const studentSubmissions = useMemo(async () => {
try {
if (itemId) {
await dispatch({
type: "assignment/fetchSubmissionsByAssignment", //here to fetch submissions in props
payload: {
id: itemId,
},
});
return this.props.submissions;
}
return this.props.submissions;
} catch (error) {
throw error.message;
}
}, []);
return (
<Table dataSource={studentSubmissions} />
)
export default SubmissionsDetail;
There is no reason to use a local component state in either the class or the function component versions. All that the local state is doing is copying the value of this.props.submissions which came from Redux. There's a whole section in the React docs about why copying props to state is bad. To summarize, it's bad because you get stale, outdated values.
Ironically, those stale values were allowing it to "work" before by covering up problems in your reducer. Your reducer is resetting the value of state.submissions every time you change the itemId, but your components are holding on to an old value (which I suspect is actually the value for the previous component? componentDidMount will not reflect a change in props).
You want your components to select a current value from Redux based on their itemId, so your reducer needs to store the submissions for every itemId separately. #Michael Hoobler's answer is correct in how to do this.
There's no problem if you want to keep using redux-saga and keep using connect but I wanted to give you a complete code so I am doing it my way which is with redux-toolkit, thunks, and react-redux hooks. The component code becomes very simple.
Component:
import React, { useEffect } from "react";
import { fetchSubmissionsByAssignment } from "../store/slice";
import { useSelector, useDispatch } from "../store";
const SubmissionsDetail = ({ itemId }) => {
const dispatch = useDispatch();
const submissions = useSelector(
(state) => state.assignment.submissionsByItem[itemId]
);
useEffect(() => {
dispatch(fetchSubmissionsByAssignment(itemId));
}, [dispatch, itemId]);
return submissions === undefined ? (
<div>Loading</div>
) : (
<div>
<div>Assignment {itemId}</div>
<div>Submissions {submissions.length}</div>
</div>
);
};
export default SubmissionsDetail;
Actions / Reducer:
import { createAsyncThunk, createReducer } from "#reduxjs/toolkit";
export const fetchSubmissionsByAssignment = createAsyncThunk(
"assignment/fetchSubmissionsByAssignment",
async (id) => {
const response = await getSubmissionsByAssignment(id);
// can you handle this in getSubmissionsByAssignment instead?
if (response.status !== 200) {
throw new Error("invalid response");
}
return {
itemId: id,
submissions: response.data.collections
};
}
);
const initialState = {
submissionsByItem: {}
};
export default createReducer(initialState, (builder) =>
builder.addCase(fetchSubmissionsByAssignment.fulfilled, (state, action) => {
const { itemId, submissions } = action.payload;
state.submissionsByItem[itemId] = submissions;
})
// could also respond to pending and rejected actions
);
if you have an object as state, and want to merge a key to the previous state - do it like this
const [myState, setMyState] = useState({key1: 'a', key2: 'b'});
setMyState(prev => {...prev, key2: 'c'});
the setter of the state hook accepts a callback that must return new state, and this callback recieves the previous state as a parameter.
Since you did not include large part of the codes, and I assume everything works in class component (including your actions and reducers). I'm just making a guess that it may be due to the omission of key.
{assignmentIds.map((itemId) => {
<SubmissionsDetail itemId={itemId} key={itemId} />
})}
OR it can be due to the other parts of our codes which were omitted.

React - Render page after props receive new data

I have few divs in my page and I want to display the latest data in them getting from an API after every 15 sec without showing the refresh of the page.
The issue is this.props have latest data but its not rendering on the page. Do I need to use componentWillUpdate ? or componentWillReceiveProps ? as both of them are obsolete.
How can I make sure to get the latest data on the page after every 15 sec.
componentDidMount(){
this.updateTimer = setInterval(() => this.getData(), 15000);
}
getData = e => {
const _this = this
admin.getDataFromAPI()
.then((response) => {
if(_this.props.getData.error){
this.context.store.dispatch(receivedDataFromAPI(response));
}
else if(_this.props.getData.data){
let updatedData = response.data;
let oldData = _this.props.getData.data;
oldData.map((col, i) => {
col.state = updatedData[i].state;
col.name = updatedData[i].name;
col.xValue = updatedData[i].xValue;
})
}
})
.catch((error) =>{
this.context.store.dispatch(failedDataFromAPI(error));
});
};
componentWillUnmount(){
clearInterval(this.updateTimer);
}
There are at least two ways of doing this
Using props you can tell parent to send different props which will cause rerender. I personally do not recommend this for sake of complexity.
Use states. Using states is really easy and I will show it to you in this example:
import React, { Component } from 'react';
class Results extends Component{
constructor(props){
super(props);
this.state = {
results : [] //Your empty array or initial value that you set
}
}
componentDidMount(){
//Here you can set interval which will make requests to API and wait for answers
//once you get response you do :
this.setState({
results : responseFromAjax
});
}
componentWillUnmount(){
}
componentDidUpdate(prevProps,prevState){
}
render(){
return(
<p>{this.state.results}</p>
)
}
}
export default Results;
In constructor of the Class Component you set its state which is basically JSON.
Once you want to change the value that is being displayed, you just call this.setState function and it pass the updated key:value pair which will replace the ones that are currently active.

How can manipulate redux data after dispatch it?

I have a search screen, contain Input And TopTabs "Songs, Artists",
When I get data from API after a search I make two things
1- I setState to appear the TopTab Component "true/false"
2- dispatch an action to save Songs & Artists Data in redux store.
that works fine.
But in topTab component, as I say before I have tow tabs "songs, artists"
For example, In the Songs component, I want to manipulate the data to achieve my case so in componentDidMount I Map the songs array from redux and push the new data into the component state.
But it's not working fine!
At the first time, I got songs from redux as empty [] although it's saved successfully in redux store when I get data from API
So how can I handle this case to not mutate the data?
Search.js "Main screen"
onSearch = async () => {
const {searchText} = this.state;
if (searchText.length > 0) {
this.setState({onBoarding: false}); // to appear the TopTab Component
try {
let response = await API.post('/search', {
name: searchText,
});
let {
data: {data},
} = response;
let artists = data.artists.data;
let songs = data.traks.data;
this.props.getResult(songs, artists);
}
catch (err) {
console.log(err);
}
}
render(){
<View style={styles.searchHeader}>
<Input
onChangeText={text => this.search(text)}
value={this.state.searchText}
onSubmitEditing={this.onSearch}
returnKeyType="search"
/>
</View>
{this.state.onBoarding ? (
<SearchBoard />
) : (
<SearchTabNavigator /> // TopTabs component
)}
}
SongsTab
...
componentDidMount() {
console.log('props.songs', this.props.songs); // Empty []
let All_tunes = [];
if (this.props.songs?.length > 0) {
console.log('mapping...');
this.props.songs.map(track =>
All_tunes.push({
id: track.id,
name: track.name,
url: URL + track.sounds,
img: URL + track.avatar,
}),
);
this.setState({All_tunes});
}
}
...
const mapStateToProps = state => {
return {
songs: state.searchResult.songs,
};
};
Edit
I fix the issue by using componentDidUpdate() life cycle
If you have any other ways tell me, please!
SongsTab
manipulateSongs = arr => {
let All_tunes = [];
arr.map(track =>
All_tunes.push({
id: track.id,
name: track.name,
url: URL + track.sounds,
img: URL + track.avatar,
}),
);
this.setState({All_tunes});
};
componentDidMount() {
if (this.props.songs?.length > 0) {
this.manipulateSongs(this.props.songs);
console.log('mapping...');
}
}
componentDidUpdate(prevProps) {
if (prevProps.songs !== this.props.songs) {
this.manipulateSongs(this.props.songs);
}
}
The problem you're referring to has to do with the way asynchronous code is handled in JavaScript (and in turn react-redux). When your component initially mounts, your redux store passes its initial state to your SongsTab.js component. That seems to be an empty array.
Any API call is an asynchronous action, and won't update the redux store until the promise has resolved/rejected and data has been successfully fetched. Any HTTP request takes much longer to complete than painting elements to the DOM. So your component loads with default data before being updated with the response from your API call a number of milliseconds later.
The way you've handled it with class-based components is fine. There are probably some optimizations you could add, but it should work as expected. You might even choose to render a Spinner component while you're fetching data from the API as well.
If you want a different approach using more modern React patterns, you can try and use the equivalent version with React hooks.
const Songs = ({ fetchSongs, songs, ...props }) => {
React.useEffect(() => {
// dispatch any redux actions upon mounting
// handle any component did update logic here as well
}, [songs])
// ...the rest of your component
}
Here are the docs for the useEffect hook.

Creating a var referencing a store from redux

I am currently working on creating a var that references a store from redux. I created one but within the render(). I want to avoid that and have it called outside of the render. Here is an example of it. I was recommended on using componentWillMount(), but I am not sure how to use it. Here is a snippet of the code I implemented. Note: It works, but only when I render the data. I am using double JSON.parse since they are strings with \
render() {
var busData= store.getState().bus.bus;
var driverData= store.getState().driver.gdriveras;
var dataReady = false;
if (busData&& driverData) {
dataReady = true;
console.log("========Parsing bus data waterout========");
var bus_data_json = JSON.parse(JSON.parse(busData));
console.log(bus_data_json);
console.log("========Parsing driver data waterout========");
var driver_data_json = JSON.parse(JSON.parse(driverData));
console.log(driver_datat_json);
busDatajson.forEach(elem => {
elem.time = getFormattedDate(elem.time)
});
driverDatajson.forEach(elem => {
elem.time = getFormattedDate(elem.time)
});
...
}
}
Here is an example of react-redux usage that will probably help you.
Don't forget to add StoreProvider to your top three component (often named App).
I warned you about the fact that React and Redux are not meant to be used by beginner javascript developer. You should consider learn about immutability and functional programming.
// ----
const driverReducer = (state, action) => {
switch (action.type) {
// ...
case 'SET_BUS': // I assume the action type
return {
...state,
gdriveras: JSON.parse(action.gdriveras) // parse your data here or even better: when you get the response
}
// ...
}
}
// same for busReducer (or where you get the bus HTTP response)
// you can also format your time properties when you get the HTTP response
// In some other file (YourComponent.js)
class YourComponent extends Component {
render() {
const {
bus,
drivers
} = this.props
if (!bus || !drivers) {
return 'loading...'
}
const formatedBus = bus.map(item => ({
...item,
time: getFormattedDate(item.time)
}))
const formatedDrivers = drivers.map(item => ({
...item,
time: getFormattedDate(item.time)
}))
// return children
}
}
// this add bus & drivers as props to your component
const mapStateToProps = state => ({
bus: state.bus.bus,
drivers: state.driver.gdriveras
})
export default connect(mapStateToProps)(YourComponent)
// you have to add StoreProvider from react-redux, otherwise connect will not be aware of your store

How to update state of component in componentDidUpdate() without being stuck in an infinite re render?

I have a component with a componentDidMount() method that calls a method called getData() which gets the initial data and sets the initial state of the component.
class LogsSettings extends React.Component {
constructor(props) {
super(props);
this.settingsUrls = [
"/ui/settings/logging"
];
this.state = {
configSettings: {},
formSchema: formSchema
};
this.configSettings = {};
this.selected = "general";
}
getData = (url, selectedSetting) => {
fetch(url)
.then((response) => {
if (response.status !== 200) {
console.log('Looks like there was a problem. Status Code: ' +
response.status);
return;
}
response.json().then((response) => {
//pass formschema here
console.log(selectedSetting);
let newFormSchema = this.setNonDefaultValues(response.data, formSchema.subsections);
Object.assign(this.configSettings, response.data);
this.setState({
configSettings : this.configSettings,
formSchema: newFormSchema
});
});
}
)
.catch((err) => {
console.log('Fetch Error :-S', err);
});
};
componentDidMount() {
this.settingsUrls.map((settingUrl) => {
this.getData(settingUrl, this.selected)
})
}
componentDidUpdate() {
this.settingsUrls.map((settingUrl) => {
this.getData(settingUrl, this.props.selectedSetting)
})
}
render() {
return (
<div className="card-wrapper">
<h2>{formSchema.label.toUpperCase()}</h2>
{
formSchema.subsections.map((subSection) => {
return (
<>
<h3>{subSection['description']}</h3>
<div style={{marginBottom: '10px'}}></div>
{
subSection['input_fields'].map((inputField) => {
return buildForm(inputField, this.handleChange)
})
}
<hr></hr>
</>
)
})
}
<button className="button button-primary">Save Changes</button>
</div>
)
}
}
The selectedSetting parameter that gets passed to the getData() method in this component will change however and when this changes, I need to change the state of the component and get new data specific to the changed selectedSetting parameter.
The new selectedSetting is passed into the component as a prop. The problem is that I can't pass the new selectedSetting parameter to my getData method to update the state of the component as it gets caught in an infinite loop.
How do I go about passing the new selectedSetting to the getData() method without getting caught in an infinite loop? Is this even possible? If not, what is the best approach I should take?
note the selectedSetting parameter isn't used in the getData() function yet but will be and it will be used to get data from an API call and a new form schema which will then lead to the ConfigSettings and formSchema states being changed
If you look closely on the lifecycle of your component, after mount, you'll fetch then update the component. This will trigger the componentDidUpdate lifecycle method which will do the same thing, causing the infinite loop. You need to have a flag that checks whether this.props.selected changed. If it didn't, don't fetch the data else fetch as normal. In the update method, you have access to the previous props. (You may also do this in componentShouldUpdate method, but it'll be just outright risky)
componentDidUpdate(prevProps) {
if( prevProps.selectedSetting !== this.props.selectedSetting ){
this.settingsUrls.map((settingUrl) => {
this.getData(settingUrl, this.props.selectedSetting)
})
}
}
also just a heads up, I noticed that your didMount method, uses a default of "general" as the selected setting, since you want to be using this.props.selectedSetting might be better if it was the one being used instead and just set default props to "general".

Categories

Resources