I am developing React + Redux single page application. I have a table with documents in page and I need to refresh data every 20 seconds. There are two functions in javascript: setTimeout and setInterval. I guess I can't use setInterval, because it just call function after some period of time. In my case I need to call function and wait for response (request processing in backend takes some time). So I used setTimeout and wrote this component (now it's simplified):
import {connect} from 'react-redux';
const { DATA_REFRESH_TIMEOUT, RETRY_REFRESH_TIMEOUT } = __ENVIRONMENT_CONFIG__;
#connect(
(state) => ({
documents: state.documents.documents,
loadingDocuments: state.documents.loading
}),
(dispatch) => bindActionCreators(
{
dispatchLoadDocuments: loadDocuments
},
dispatch
)
)
export default class Dashboard extends Component {
documentasTimeoutId;
constructor(props) {
super(props);
this.state = {
documentType: null
};
}
....
handleDocumentTypeChange = (event, documentType) => {
//If document type was changed I must to abort current timer
//and get documents with particular type immediately
this.setState({documentType: documentType});
this.clearTimeoutAndGetDocuments(documentType);
};
getDocuments = (documentType) => {
//Here I am checking for document loading phase
//If it is loading, we will wait and repeat loading after short time
const{ loadingDocuments } = this.props;
if(!loadingDocuments) {
this.props.dispatchLoadDocuments(documentType);
} else {
this.documentasTimeoutId = setTimeout(() => { this.getDocuments(documentType); }, RETRY_REFRESH_TIMEOUT);
}
};
clearTimeoutAndGetDocuments = (documentType) => {
//Abort delayed data getting and get data immediately
clearTimeout(this.documentasTimeoutId);
this.getDocuments(documentType);
};
componentDidMount(){
//Load documents on start up
this.props.dispatchLoadDocuments();
}
componentWillReceiveProps(newProps) {
//Here I trying to get event when documents loaded
let areDocumentsJustLoaded = this.props.loadingDocuments && !newProps.loadingDocuments;
if(areDocumentsJustLoaded) {
//If they loaded, I am setting timeout to refresh documents after some time
this.documentasTimeoutId = setTimeout(() => { this.getOutstandingFailures(this.state.search); }, DATA_REFRESH_TIMEOUT);
}
}
render() {
const {columns, documents} = this.props;
return (
//.....
<DataTable
columns={columns}
source={documents}
title="Documents"
name="documents"
emptyMessage="No data"/>
//....
);
}
}
As you can see I'm getting documents and loadingDocuments from reducer. I put documents to my DataTable, and by loadingDocuments changes I can define when data loading completed.
It's working, but I'am not sure for correct react and redux using (I am a newbie in React/Redux). Maybe there a better approach to do same actions? Maybe we can somehow create a separate component for this purpose and reuse it in other pages?
Related
so i have a bit of a weird problem i dont know how to solve.
In my code i have a custom hook with a bunch of functionality for a fetching a list
of train journeys. I have some useEffects to that keeps loading in new journeys untill the last journey of the day.
When i change route, while it is still loading in new journeys. I get the "changes to unmounted component" React error.
I understand that i get this error because the component is doing an async fetch that finishes after i've gone to a new page.
The problem i can't figure out is HOW do i prevent it from doing that? the "unmounted" error always occur on one of the 4 lines listed in the code snippet.
Mock of the code:
const [loading, setLoading] = useState(true);
const [journeys, setJourneys] = useState([]);
const [hasLaterDepartures, setHasLaterDepartures] = useState(true);
const getJourneys = async (date, journeys) => {
setLoading(true);
setHasLaterDepartures(true);
const selectedDateJourneys = await fetchJourney(date); // Fetch that returns 0-3 journeys
if (condition1) setHasLaterDepartures(false); // trying to update unmounted component
if (condition2) {
if (condition3) {
setJourneys(something1); // trying to update unmounted component
} else {
setJourneys(something2) // trying to update unmounted component
}
} else {
setJourneys(something3); // trying to update unmounted component
}
};
// useEffects for continous loading of journeys.
useEffect(() => {
if (!hasLaterDepartures) setLoading(false);
}, [hasLaterDepartures]);
useEffect(() => {
if (hasLaterDepartures && journeys.length > 0) {
const latestStart = ... // just a date
if (latestStart.addMinutes(5).isSameDay(latestStart)) {
getJourneys(latestStart.addMinutes(5), journeys);
} else {
setLoading(false);
}
}
}, [journeys]);
I can't use a variable like isMounted = true in the useEffect beacuse it would reach inside the if statement and reach a "setState" by the time i'm on another page.
Moving the entire call into a useEffect doesn't seem to work either. I am at a loss.
Create a variable called mounted with useRef, initialised as true. Then add an effect to set mounted.current to false when the component unmounts.
You can use mounted.current anywhere inside the component to see if it's mounted, and check that before setting any state.
useRef gives you a variable you can mutate but which doesn't cause a rerender.
When you use useEffect hook with action which can be done after component change you should also take care about clean effect when needed. Maybe example help you, also check this page.
useEffect(() => {
let isClosed = false
const fetchData = async () => {
const data = await response.json()
if ( !isClosed ) {
setState( data )
}
};
fetchData()
return () => {
isClosed = true
};
}, []);
In your use case, you probably want to create a Store that doesn't reload everytime you change route (client side).
Example of a store using useContext();
const MyStoreContext = createContext()
export function useMyStore() {
const context = useContext(MyStoreContext)
if (!context && typeof window !== 'undefined') {
throw new Error(`useMyStore must be used within a MyStoreContext`)
}
return context
}
export function MyStoreProvider(props) {
const [ myState, setMyState ] = useState()
//....whatever codes u doing with ur hook.
const exampleCustomFunction = () => {
return myState
}
const getAllRoutes = async (mydestination) => {
return await getAllMyRoutesFromApi(mydestination)
}
// you return all your "getter" and "setter" in value props so you can use them outside the store.
return <MyStoreContext.Provider value={{ myState, setMyState, exampleCustomFunction, getAllRoutes }}>{props.children}</MyStoreContext.Provider>
}
You will wrap the store around your entire App, e.g.
<MyStoreProvider>
<App />
</MyStoreProvider>
In your page where you want to use your hook, you can do
const { myState, setMyState, exampleCustomFunction, getAllRoutes } = useMyStore()
const onClick = async () => getAllRouters(mydestination)
Considering if you have client side routing (not server side), this doesn't get reloaded every time you change your route.
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.
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.
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
}
})
}
}
I have a component that displays search data returned from the Spotify API. However, every time I update the state the UI flickers:
Input:
<DebounceInput
debounceTimeout={300}
onChange={handleChange}
/>
Hook:
const [searchResults, setSearchResults] = useState(null)
API call w/ Apollo:
const searchSpotify = async (query) => {
const result = await props.client.query({
query: SearchTracks,
variables: {
query
}
})
const tracks = result.data.searchedTracks
setSearchResults(tracks)
}
Render:
{searchResults &&
<div className="search-results">
{searchResults.map((song) => (
<SongInfo key={song.id} {...song} />
))}
</div>
}
I noticed it only happens on the first load. For example, if I were to type the query again it shows without flickering. Is there a better way to implement this so the UI doesn't flicker?
Below are the frames that cause the flicker. What I think is happening is it takes some time for the images to load. While they are loading the items have reduced height. You should make sure SongInfo layout does not depend on whether the image has been loaded or not.
Images not loaded - items are collapsed:
Images were loaded:
I think whats happening is that you are executing a search query on every key stroke which is causing the weird behavior.
Use lodash debounce to avoid doing a search on every key stroke.
That should address the flickering. (Also, adding a loading state will help)
Sample debounce component
import React, {Component} from 'react'
import { debounce } from 'lodash'
class TableSearch extends Component {
//********************************************/
constructor(props){
super(props)
this.state = {
value: props.value
}
this.changeSearch = debounce(this.props.changeSearch, 250)
}
//********************************************/
handleChange = (e) => {
const val = e.target.value
this.setState({ value: val }, () => {
this.changeSearch(val)
})
}
//********************************************/
render() {
return (
<input
onChange = {this.handleChange}
value = {this.props.value}
/>
)
}
//********************************************/
}